mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-08-07 19:08:32 -05:00
Compare commits
486 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ed5c50db7d | ||
![]() |
b463428a40 | ||
![]() |
d3ab4d2f11 | ||
![]() |
0a469cfdd1 | ||
![]() |
fc82121604 | ||
![]() |
25444034ab | ||
![]() |
eec1dbe0a0 | ||
![]() |
47e8bdb752 | ||
![]() |
75fc373b51 | ||
![]() |
8a397034fd | ||
![]() |
7f0f48ddac | ||
![]() |
e92046a265 | ||
![]() |
fe00dffb70 | ||
![]() |
a02ddeb722 | ||
![]() |
9b3bc62132 | ||
![]() |
d3fda57b6d | ||
![]() |
19fddc8da8 | ||
![]() |
e452c161ba | ||
![]() |
e783494022 | ||
![]() |
ca3d62f377 | ||
![]() |
0584ceb157 | ||
![]() |
633b0dd928 | ||
![]() |
cad2f77490 | ||
![]() |
a40448a350 | ||
![]() |
01df596dc7 | ||
![]() |
176b0416cd | ||
![]() |
b0449d59da | ||
![]() |
804222d68a | ||
![]() |
858bca0f7d | ||
![]() |
ac459b84c6 | ||
![]() |
716005fbd2 | ||
![]() |
086dccc177 | ||
![]() |
e13dbe4881 | ||
![]() |
6a16bdf5fd | ||
![]() |
f3b46f50bf | ||
![]() |
14a2ad2b0d | ||
![]() |
fb539865e0 | ||
![]() |
7d92caccf0 | ||
![]() |
2e71eee7b4 | ||
![]() |
507085ee7b | ||
![]() |
4690b273cc | ||
![]() |
335bdb820f | ||
![]() |
b17d9f850e | ||
![]() |
7f8ba75d90 | ||
![]() |
842b951549 | ||
![]() |
f373211281 | ||
![]() |
c5500db9ef | ||
![]() |
e107d5df6f | ||
![]() |
6e84668884 | ||
![]() |
63d2dcc1f7 | ||
![]() |
0ee6426eb5 | ||
![]() |
ac2cac6edc | ||
![]() |
73682d22d6 | ||
![]() |
bef80037da | ||
![]() |
056b9638ab | ||
![]() |
be94a8e49a | ||
![]() |
7587150f96 | ||
![]() |
e97b06674c | ||
![]() |
e82700a826 | ||
![]() |
05c16e1539 | ||
![]() |
50fa69aca4 | ||
![]() |
cb3001ac3b | ||
![]() |
9bbcb9319c | ||
![]() |
32f371fcb6 | ||
![]() |
16559e83f5 | ||
![]() |
052c8c5372 | ||
![]() |
8268607a56 | ||
![]() |
c33e9245bf | ||
![]() |
4d5166d568 | ||
![]() |
fdc8060071 | ||
![]() |
e139ce77ee | ||
![]() |
ed6f2e40cf | ||
![]() |
7e36986a26 | ||
![]() |
426ad30a52 | ||
![]() |
111ed38cce | ||
![]() |
e07128a145 | ||
![]() |
610fa075f6 | ||
![]() |
5e75d84920 | ||
![]() |
4d97a825d9 | ||
![]() |
bac739a6d9 | ||
![]() |
d935dcd350 | ||
![]() |
be2061b74d | ||
![]() |
2aa2086dfb | ||
![]() |
a89d4ee434 | ||
![]() |
cd5d762cbc | ||
![]() |
a96ab9a9a4 | ||
![]() |
f0a1aed029 | ||
![]() |
7b56ad9dad | ||
![]() |
e05f365e6a | ||
![]() |
c15e94f759 | ||
![]() |
d93ec0d5c7 | ||
![]() |
74cf5373b9 | ||
![]() |
4ddc034e9c | ||
![]() |
061dc04755 | ||
![]() |
0e4597131b | ||
![]() |
57f77c4657 | ||
![]() |
89d6e422f5 | ||
![]() |
520f92503f | ||
![]() |
4cef4adc7e | ||
![]() |
e97ff3d671 | ||
![]() |
97e96d02f2 | ||
![]() |
654ee4e62e | ||
![]() |
87fe1be1ed | ||
![]() |
4d7d3afc7d | ||
![]() |
7bca5bf40e | ||
![]() |
158e83c425 | ||
![]() |
fa0ccff5b2 | ||
![]() |
a0505aa8e9 | ||
![]() |
86677ee6b4 | ||
![]() |
eb91ac0c3c | ||
![]() |
659c7cdbd8 | ||
![]() |
ef9d8c64fc | ||
![]() |
e6927a196f | ||
![]() |
729e5ee0e1 | ||
![]() |
a3f214b6ba | ||
![]() |
a2f0a9687d | ||
![]() |
820920839d | ||
![]() |
0d5d8f7c80 | ||
![]() |
bf198f37db | ||
![]() |
c6af2044ce | ||
![]() |
a9b331c5fd | ||
![]() |
cb88ffff79 | ||
![]() |
bcf17bfdc0 | ||
![]() |
110ef17ed8 | ||
![]() |
348d07757b | ||
![]() |
eaeda47690 | ||
![]() |
fdf330276e | ||
![]() |
dace3dc803 | ||
![]() |
70d48b39f3 | ||
![]() |
d9f5bc7681 | ||
![]() |
52aa78acab | ||
![]() |
b73ec76146 | ||
![]() |
70e49a61fa | ||
![]() |
7904a3efb7 | ||
![]() |
717a4951fd | ||
![]() |
a3b4349b1c | ||
![]() |
4b74cd5677 | ||
![]() |
6d554bace1 | ||
![]() |
c4367818b7 | ||
![]() |
ebdddc4fe4 | ||
![]() |
bac0dbb70b | ||
![]() |
0a7412424d | ||
![]() |
d5601b7ec0 | ||
![]() |
b79d88d5ef | ||
![]() |
851d9cc313 | ||
![]() |
40ef375c15 | ||
![]() |
de32addf76 | ||
![]() |
1e541b688d | ||
![]() |
c05bfb894a | ||
![]() |
279e269a66 | ||
![]() |
8490b65bcc | ||
![]() |
1234634ba3 | ||
![]() |
3d2d9cb3a2 | ||
![]() |
7740961697 | ||
![]() |
22c77e7be5 | ||
![]() |
6566e36141 | ||
![]() |
a4e5d36a02 | ||
![]() |
54a0da8151 | ||
![]() |
dc525783ee | ||
![]() |
a5ae056c9b | ||
![]() |
e0c87fc556 | ||
![]() |
fb38aacde4 | ||
![]() |
564f3b9170 | ||
![]() |
9cb14e2815 | ||
![]() |
54c023523f | ||
![]() |
f090537ef4 | ||
![]() |
be7bf5288b | ||
![]() |
71d7aa3fb2 | ||
![]() |
1b8c4bb1a5 | ||
![]() |
fddda75f75 | ||
![]() |
aa7e2594e2 | ||
![]() |
5236f4e58d | ||
![]() |
70cb27bf0a | ||
![]() |
2a744a3a83 | ||
![]() |
2e544326e5 | ||
![]() |
d42c13a9c6 | ||
![]() |
020696fb17 | ||
![]() |
06bf3e27d3 | ||
![]() |
3b17f9d6ec | ||
![]() |
9c8b43f602 | ||
![]() |
70f052cb5c | ||
![]() |
e5c10fcd93 | ||
![]() |
9eb377703e | ||
![]() |
4c63dad309 | ||
![]() |
45e52aa985 | ||
![]() |
6066d00c5e | ||
![]() |
59a10a8127 | ||
![]() |
6dc8edea88 | ||
![]() |
ef1ee8e0c0 | ||
![]() |
1428b26703 | ||
![]() |
63b841c496 | ||
![]() |
208f4291d6 | ||
![]() |
750d08ec01 | ||
![]() |
8d665aeac4 | ||
![]() |
a0f983f05d | ||
![]() |
1df922ef16 | ||
![]() |
beed820602 | ||
![]() |
b6722fdd84 | ||
![]() |
eb47fb5501 | ||
![]() |
aacf7ba275 | ||
![]() |
400392655e | ||
![]() |
713985f259 | ||
![]() |
664280ccce | ||
![]() |
9fe6c5c3a9 | ||
![]() |
ab5098a036 | ||
![]() |
b092bb6848 | ||
![]() |
17ded12375 | ||
![]() |
118149c539 | ||
![]() |
57da323cea | ||
![]() |
ebe2ba401d | ||
![]() |
a67443c195 | ||
![]() |
2e0d36c4d9 | ||
![]() |
db4b621631 | ||
![]() |
a91958bfe1 | ||
![]() |
5395208b00 | ||
![]() |
d909010f08 | ||
![]() |
5c7b65163e | ||
![]() |
9f18d0ad45 | ||
![]() |
2de3894d67 | ||
![]() |
03c6a4e18e | ||
![]() |
f9ab8d3b35 | ||
![]() |
ef63ec40d9 | ||
![]() |
761a6a4264 | ||
![]() |
8139ecfd39 | ||
![]() |
fb09f67899 | ||
![]() |
b8e8bf3dd4 | ||
![]() |
d690b34ee0 | ||
![]() |
e24b40de29 | ||
![]() |
6581cff8dc | ||
![]() |
5c3ae44021 | ||
![]() |
2b341afcae | ||
![]() |
7f4cfc0b76 | ||
![]() |
b2327d6fde | ||
![]() |
f964dd5935 | ||
![]() |
4ecd5ada06 | ||
![]() |
f770f0444a | ||
![]() |
39d1c051cf | ||
![]() |
5622e13802 | ||
![]() |
d6285cc851 | ||
![]() |
777d8b4609 | ||
![]() |
edb9264d78 | ||
![]() |
6b708c5ed3 | ||
![]() |
1dd2386fa5 | ||
![]() |
e6164eb1ab | ||
![]() |
bd01b821ec | ||
![]() |
b44ca770a5 | ||
![]() |
9093ac5901 | ||
![]() |
755da317ea | ||
![]() |
6834c70bae | ||
![]() |
1297c0911c | ||
![]() |
ebcf4e2d81 | ||
![]() |
bd3a2306d6 | ||
![]() |
27666da4e9 | ||
![]() |
70f7b614e6 | ||
![]() |
7d86ee32af | ||
![]() |
70c02a1c82 | ||
![]() |
39637fc4aa | ||
![]() |
1de7a490b4 | ||
![]() |
6d4aa76405 | ||
![]() |
27ae4f6b1e | ||
![]() |
d6e733c56f | ||
![]() |
aa6e96e54d | ||
![]() |
3d173a13ab | ||
![]() |
7beb8a0929 | ||
![]() |
8af0259671 | ||
![]() |
67953c98a9 | ||
![]() |
527c533958 | ||
![]() |
544ca8d008 | ||
![]() |
bd02c78966 | ||
![]() |
9e311241b3 | ||
![]() |
e228e18f04 | ||
![]() |
e2bea3aee3 | ||
![]() |
08beaf81d5 | ||
![]() |
86079a936e | ||
![]() |
c6acf2f7f6 | ||
![]() |
28b7c3c208 | ||
![]() |
75c8cd9967 | ||
![]() |
4fb5dce5e7 | ||
![]() |
b8e7506de4 | ||
![]() |
131ebf0480 | ||
![]() |
80420a99f5 | ||
![]() |
6a70369a77 | ||
![]() |
fb83069975 | ||
![]() |
f61ecadf76 | ||
![]() |
f040c4e593 | ||
![]() |
c2a47ca4b1 | ||
![]() |
a283b815ef | ||
![]() |
3dd4583ea8 | ||
![]() |
bf79b252ad | ||
![]() |
a2ad62b310 | ||
![]() |
4b9a8f3409 | ||
![]() |
802bd7fb0d | ||
![]() |
320298e3ff | ||
![]() |
5e5059b2e7 | ||
![]() |
d6d7528668 | ||
![]() |
3f8c74c4af | ||
![]() |
0a1b810da7 | ||
![]() |
25fb9fb185 | ||
![]() |
c547128238 | ||
![]() |
99b5e25731 | ||
![]() |
20e724da46 | ||
![]() |
03838421bc | ||
![]() |
b9cf517cd1 | ||
![]() |
f7f78d80b7 | ||
![]() |
e63b820c07 | ||
![]() |
2ac6a02e31 | ||
![]() |
6e79b771ec | ||
![]() |
37caf6a64a | ||
![]() |
198354d07d | ||
![]() |
eec9716ffa | ||
![]() |
64d0c7fae6 | ||
![]() |
c54d26ed19 | ||
![]() |
cffe9fa354 | ||
![]() |
46b0776714 | ||
![]() |
929b25a969 | ||
![]() |
3c2fac3d28 | ||
![]() |
9a165e87fd | ||
![]() |
95c4e77ae4 | ||
![]() |
ba3696566f | ||
![]() |
6589369e1b | ||
![]() |
9c2c74ad2b | ||
![]() |
b7c118afa3 | ||
![]() |
e9b5f8d9f8 | ||
![]() |
7d676a75a8 | ||
![]() |
544e8db722 | ||
![]() |
ba066af664 | ||
![]() |
c4286d0a48 | ||
![]() |
1c535e6ff1 | ||
![]() |
0b9ea5c60f | ||
![]() |
0fa3c3188d | ||
![]() |
ebcb2cf694 | ||
![]() |
8dc15352ef | ||
![]() |
403de42031 | ||
![]() |
b08a5e62d8 | ||
![]() |
978bb3c339 | ||
![]() |
3e389a3140 | ||
![]() |
0dbd598116 | ||
![]() |
01d10da061 | ||
![]() |
0aad31b4bc | ||
![]() |
2f599ca1d3 | ||
![]() |
cd8f99d2c3 | ||
![]() |
6968e228e1 | ||
![]() |
b7d310ef90 | ||
![]() |
1c3b85249c | ||
![]() |
147a8774c6 | ||
![]() |
406f8daa12 | ||
![]() |
0a4a06b991 | ||
![]() |
41e842a9e6 | ||
![]() |
1fb3316436 | ||
![]() |
2f26d07480 | ||
![]() |
360fc081bb | ||
![]() |
4cae338479 | ||
![]() |
9ace199422 | ||
![]() |
f42e5bf1e5 | ||
![]() |
5863060585 | ||
![]() |
e466158ce2 | ||
![]() |
38156123d4 | ||
![]() |
ae3e2a7063 | ||
![]() |
48d83e166b | ||
![]() |
29e9d7d793 | ||
![]() |
dcbc0ea2e5 | ||
![]() |
c527b274b6 | ||
![]() |
5892941d8a | ||
![]() |
b653e44f65 | ||
![]() |
e75534c0f2 | ||
![]() |
0e93f7eba5 | ||
![]() |
665863e395 | ||
![]() |
d9e3895f34 | ||
![]() |
f06e2c1089 | ||
![]() |
c6b9e2b544 | ||
![]() |
240d5b9da2 | ||
![]() |
b10e7abbe8 | ||
![]() |
01c2fe508e | ||
![]() |
ee31fdc650 | ||
![]() |
fa7b90a584 | ||
![]() |
37c2051e01 | ||
![]() |
3561935e1b | ||
![]() |
7669679fb1 | ||
![]() |
2e44c18cdb | ||
![]() |
86da578774 | ||
![]() |
46f778111c | ||
![]() |
7dfcc7f47b | ||
![]() |
39b35c090b | ||
![]() |
ee4e026ba2 | ||
![]() |
de6ba3489a | ||
![]() |
6381093386 | ||
![]() |
fd6bfd02ce | ||
![]() |
1da652ba4d | ||
![]() |
9a4190bedf | ||
![]() |
561db8607a | ||
![]() |
7f9a0204b5 | ||
![]() |
32224f187d | ||
![]() |
d91fa99e77 | ||
![]() |
3f94fc2618 | ||
![]() |
e79c45c98d | ||
![]() |
400da7bbc5 | ||
![]() |
bb814da95b | ||
![]() |
fad3df1e39 | ||
![]() |
1b1b57eb6a | ||
![]() |
57a5a4147b | ||
![]() |
1041504cb1 | ||
![]() |
26b40e06f8 | ||
![]() |
df8235de13 | ||
![]() |
12e45624db | ||
![]() |
37237dfcf6 | ||
![]() |
da3695e3a4 | ||
![]() |
24c53e78a7 | ||
![]() |
275bd96ba8 | ||
![]() |
01d448ecde | ||
![]() |
3cdd38cb70 | ||
![]() |
f184e6b162 | ||
![]() |
55a6dca373 | ||
![]() |
5517c049b8 | ||
![]() |
86f83f2bc4 | ||
![]() |
557abbc17e | ||
![]() |
e0293db16d | ||
![]() |
fbb2da42dc | ||
![]() |
2d841e7167 | ||
![]() |
f214fe1b3e | ||
![]() |
e5fe515b69 | ||
![]() |
dfb88ebf83 | ||
![]() |
789abb3bbb | ||
![]() |
273c474e3f | ||
![]() |
b262ec4b32 | ||
![]() |
c05de3d57f | ||
![]() |
55c4c690ef | ||
![]() |
e10a2391c4 | ||
![]() |
9b244d0265 | ||
![]() |
cfc1ca45fc | ||
![]() |
1c4e3f682e | ||
![]() |
75b22e8684 | ||
![]() |
ca2cb694d0 | ||
![]() |
cea34211b6 | ||
![]() |
d252d040bf | ||
![]() |
43c88ae0f0 | ||
![]() |
93be4e98d5 | ||
![]() |
659cd3e9d5 | ||
![]() |
2c3eaadbce | ||
![]() |
1947d67e69 | ||
![]() |
35dcc54dc8 | ||
![]() |
48796e6961 | ||
![]() |
164418880a | ||
![]() |
fbca412d30 | ||
![]() |
70347bb8f3 | ||
![]() |
eaf11ea134 | ||
![]() |
a59f8cd1b1 | ||
![]() |
e9affbc1cf | ||
![]() |
aa8789ae31 | ||
![]() |
5c310c51d4 | ||
![]() |
cf3fa50b55 | ||
![]() |
7f933d373f | ||
![]() |
ece94379d8 | ||
![]() |
677cfb7a1e | ||
![]() |
fb9d750684 | ||
![]() |
b8469946a8 | ||
![]() |
03f071fd27 | ||
![]() |
34c42c4339 | ||
![]() |
b45bd66573 | ||
![]() |
3b2bc292d8 | ||
![]() |
8b57967836 | ||
![]() |
2dc3019083 | ||
![]() |
b5a85caa72 | ||
![]() |
bb9b438aa6 | ||
![]() |
7906d8fef1 | ||
![]() |
2374506a20 | ||
![]() |
a85792e327 | ||
![]() |
f5df910894 | ||
![]() |
80b47fa287 | ||
![]() |
f0ad92e542 | ||
![]() |
d1d09ac6ac | ||
![]() |
66240188c7 | ||
![]() |
d1f285113d | ||
![]() |
56dfc71bb9 | ||
![]() |
a8f27f79dd | ||
![]() |
2c702eb568 | ||
![]() |
63a58ccc38 | ||
![]() |
4b0027797a | ||
![]() |
66d6d29c23 | ||
![]() |
fbb3a069cd | ||
![]() |
51b1528fee | ||
![]() |
abd54eeb3a | ||
![]() |
5369e0be03 | ||
![]() |
8699b6931c | ||
![]() |
35124023f0 | ||
![]() |
fd4c9a1758 | ||
![]() |
32186e0de1 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -85,3 +85,4 @@ scripts/nuke
|
||||
|
||||
# this is where the compiled frontend is moved to.
|
||||
/src/documents/static/frontend/
|
||||
/docs/.vscode/settings.json
|
||||
|
3
Pipfile
3
Pipfile
@@ -40,8 +40,9 @@ whitenoise = "~=5.2.0"
|
||||
watchdog = "*"
|
||||
whoosh="~=2.7.4"
|
||||
inotifyrecursive = "~=0.3.4"
|
||||
ocrmypdf = "*"
|
||||
ocrmypdf = "~=11.4.5"
|
||||
tqdm = "*"
|
||||
tika = "*"
|
||||
|
||||
[dev-packages]
|
||||
coveralls = "*"
|
||||
|
629
Pipfile.lock
generated
629
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "3d576f289958226a7583e4c471c7f8c11bff6933bf093185f623cfb381a92412"
|
||||
"sha256": "c35d84fd7f4f1c7d599039712362935e7c41a226b0ab3d83d8c1c2fb2ad0962a"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -44,6 +44,13 @@
|
||||
],
|
||||
"version": "==1.17.12"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
|
||||
"sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
|
||||
],
|
||||
"version": "==2020.12.5"
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
"sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e",
|
||||
@@ -89,50 +96,40 @@
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
|
||||
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
|
||||
],
|
||||
"markers": "python_version >= '3.1'",
|
||||
"version": "==3.0.4"
|
||||
"version": "==4.0.0"
|
||||
},
|
||||
"coloredlogs": {
|
||||
"hashes": [
|
||||
"sha256:346f58aad6afd48444c2468618623638dadab76e4e70d5e10822676f2d32226a",
|
||||
"sha256:a1fab193d2053aa6c0a97608c4342d031f1f93a3d1218432c59322441d31a505",
|
||||
"sha256:b0c2124367d4f72bd739f48e1f61491b4baf145d6bda33b606b4a53cb3f96a97"
|
||||
"sha256:5e78691e2673a8e294499e1832bb13efcfb44a86b92e18109fa18951093218ab",
|
||||
"sha256:b7f630a8297a66984b6bae0f6a1b0e0afb9f2f6838ea3bfa58f50d3d13e133d6"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==14.0"
|
||||
"version": "==15.0"
|
||||
},
|
||||
"cryptography": {
|
||||
"hashes": [
|
||||
"sha256:07ca431b788249af92764e3be9a488aa1d39a0bc3be313d826bbec690417e538",
|
||||
"sha256:13b88a0bd044b4eae1ef40e265d006e34dbcde0c2f1e15eb9896501b2d8f6c6f",
|
||||
"sha256:257dab4f368fae15f378ea9a4d2799bf3696668062de0e9fa0ebb7a738a6917d",
|
||||
"sha256:32434673d8505b42c0de4de86da8c1620651abd24afe91ae0335597683ed1b77",
|
||||
"sha256:3cd75a683b15576cfc822c7c5742b3276e50b21a06672dc3a800a2d5da4ecd1b",
|
||||
"sha256:4e7268a0ca14536fecfdf2b00297d4e407da904718658c1ff1961c713f90fd33",
|
||||
"sha256:545a8550782dda68f8cdc75a6e3bf252017aa8f75f19f5a9ca940772fc0cb56e",
|
||||
"sha256:55d0b896631412b6f0c7de56e12eb3e261ac347fbaa5d5e705291a9016e5f8cb",
|
||||
"sha256:5849d59358547bf789ee7e0d7a9036b2d29e9a4ddf1ce5e06bb45634f995c53e",
|
||||
"sha256:59f7d4cfea9ef12eb9b14b83d79b432162a0a24a91ddc15c2c9bf76a68d96f2b",
|
||||
"sha256:6dc59630ecce8c1f558277ceb212c751d6730bd12c80ea96b4ac65637c4f55e7",
|
||||
"sha256:7117319b44ed1842c617d0a452383a5a052ec6aa726dfbaffa8b94c910444297",
|
||||
"sha256:75e8e6684cf0034f6bf2a97095cb95f81537b12b36a8fedf06e73050bb171c2d",
|
||||
"sha256:7b8d9d8d3a9bd240f453342981f765346c87ade811519f98664519696f8e6ab7",
|
||||
"sha256:a035a10686532b0587d58a606004aa20ad895c60c4d029afa245802347fab57b",
|
||||
"sha256:a4e27ed0b2504195f855b52052eadcc9795c59909c9d84314c5408687f933fc7",
|
||||
"sha256:a733671100cd26d816eed39507e585c156e4498293a907029969234e5e634bc4",
|
||||
"sha256:a75f306a16d9f9afebfbedc41c8c2351d8e61e818ba6b4c40815e2b5740bb6b8",
|
||||
"sha256:bd717aa029217b8ef94a7d21632a3bb5a4e7218a4513d2521c2a2fd63011e98b",
|
||||
"sha256:d25cecbac20713a7c3bc544372d42d8eafa89799f492a43b79e1dfd650484851",
|
||||
"sha256:d26a2557d8f9122f9bf445fc7034242f4375bd4e95ecda007667540270965b13",
|
||||
"sha256:d3545829ab42a66b84a9aaabf216a4dce7f16dbc76eb69be5c302ed6b8f4a29b",
|
||||
"sha256:d3d5e10be0cf2a12214ddee45c6bd203dab435e3d83b4560c03066eda600bfe3",
|
||||
"sha256:efe15aca4f64f3a7ea0c09c87826490e50ed166ce67368a68f315ea0807a20df"
|
||||
"sha256:0003a52a123602e1acee177dc90dd201f9bb1e73f24a070db7d36c588e8f5c7d",
|
||||
"sha256:0e85aaae861d0485eb5a79d33226dd6248d2a9f133b81532c8f5aae37de10ff7",
|
||||
"sha256:594a1db4511bc4d960571536abe21b4e5c3003e8750ab8365fafce71c5d86901",
|
||||
"sha256:69e836c9e5ff4373ce6d3ab311c1a2eed274793083858d3cd4c7d12ce20d5f9c",
|
||||
"sha256:788a3c9942df5e4371c199d10383f44a105d67d401fb4304178020142f020244",
|
||||
"sha256:7e177e4bea2de937a584b13645cab32f25e3d96fc0bc4a4cf99c27dc77682be6",
|
||||
"sha256:83d9d2dfec70364a74f4e7c70ad04d3ca2e6a08b703606993407bf46b97868c5",
|
||||
"sha256:84ef7a0c10c24a7773163f917f1cb6b4444597efd505a8aed0a22e8c4780f27e",
|
||||
"sha256:982f661bffc7a24b6d4f8ebe3291f17cf3833a0941c6f4d9d55c790b9aa2cdb3",
|
||||
"sha256:9e21301f7a1e7c03dbea73e8602905a4ebba641547a462b26dd03451e5769e7c",
|
||||
"sha256:9f6b0492d111b43de5f70052e24c1f0951cb9e6022188ebcb1cc3a3d301469b0",
|
||||
"sha256:a69bd3c68b98298f490e84519b954335154917eaab52cf582fa2c5c7efc6e812",
|
||||
"sha256:b4890d5fb9b7a23e3bf8abf5a8a7da8e228f1e97dc96b30b95685df840b6914a",
|
||||
"sha256:c366df0401d1ec4e548bebe8f91d55ebcc0ec3137900d214dd7aac8427ef3030",
|
||||
"sha256:dc42f645f8f3a489c3dd416730a514e7a91a59510ddaadc09d04224c098d3302"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==3.2.1"
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||
"version": "==3.3.1"
|
||||
},
|
||||
"dateparser": {
|
||||
"hashes": [
|
||||
@@ -144,19 +141,19 @@
|
||||
},
|
||||
"django": {
|
||||
"hashes": [
|
||||
"sha256:5c866205f15e7a7123f1eec6ab939d22d5bde1416635cab259684af66d8e48a2",
|
||||
"sha256:edb10b5c45e7e9c0fb1dc00b76ec7449aca258a39ffd613dbd078c51d19c9f03"
|
||||
"sha256:2d78425ba74c7a1a74b196058b261b9733a8570782f4e2828974777ccca7edf7",
|
||||
"sha256:efa2ab96b33b20c2182db93147a0c3cd7769d418926f9e9f140a60dca7c64ca9"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.1.4"
|
||||
"version": "==3.1.5"
|
||||
},
|
||||
"django-cors-headers": {
|
||||
"hashes": [
|
||||
"sha256:9322255c296d5f75089571f29e520c83ff9693df17aa3cf9f6a4bea7c6740169",
|
||||
"sha256:db82b2840f667d47872ae3e4a4e0a0d72fbecb42779b8aa233fa8bb965f7836a"
|
||||
"sha256:5665fc1b1aabf1b678885cf6f8f8bd7da36ef0a978375e767d491b48d3055d8f",
|
||||
"sha256:ba898dd478cd4be3a38ebc3d8729fa4d044679f8c91b2684edee41129d7e968a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.5.0"
|
||||
"version": "==3.6.0"
|
||||
},
|
||||
"django-extensions": {
|
||||
"hashes": [
|
||||
@@ -192,7 +189,8 @@
|
||||
},
|
||||
"djangorestframework": {
|
||||
"hashes": [
|
||||
"sha256:0209bafcb7b5010fdfec784034f059d512256424de2a0f084cb82b096d6dd6a7"
|
||||
"sha256:0209bafcb7b5010fdfec784034f059d512256424de2a0f084cb82b096d6dd6a7",
|
||||
"sha256:0898182b4737a7b584a2c73735d89816343369f259fea932d90dc78e35d8ac33"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.12.2"
|
||||
@@ -223,19 +221,28 @@
|
||||
},
|
||||
"humanfriendly": {
|
||||
"hashes": [
|
||||
"sha256:175ffa628aa76da2c17369a5da5856084562cc66dfe7f82ae93ca3ef175277a6",
|
||||
"sha256:3c9ab8d28e88e6cc998e41963357736dafd555ee5bb666b50e42f6ce28dd3e3d"
|
||||
"sha256:066562956639ab21ff2676d1fda0b5987e985c534fc76700a19bd54bcb81121d",
|
||||
"sha256:d5c731705114b9ad673754f3317d9fa4c23212f36b29bdc4272a892eafc9bc72"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==9.0"
|
||||
"version": "==9.1"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:4a57a6379512ade94fa99e2fa46d3cd0f2f553040548d0e2958c6ed90ee48226",
|
||||
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.10"
|
||||
},
|
||||
"imap-tools": {
|
||||
"hashes": [
|
||||
"sha256:72bf46dc135b039a5d5b59f4e079242ac15eac02a30038e8cb2dec7b153cab65",
|
||||
"sha256:75dc1c72dd76d9e577df26a1e0ec3a809b5eebce77678851458dcd2eae127ac9"
|
||||
"sha256:7d2d25b35117a3750c3b561dd93cc2fcb24cdc457830a049796c639f4371e317",
|
||||
"sha256:80088839cd1959f20c44206cdad4463ca1e7647ff67cf5b0e31e810fb6aaa6c4"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.33.0"
|
||||
"version": "==0.34.0"
|
||||
},
|
||||
"img2pdf": {
|
||||
"hashes": [
|
||||
@@ -246,11 +253,11 @@
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:6112e21359ef8f344e7178aa5b72dc6e62b38b0d008e6d3cb212c5b84df72013",
|
||||
"sha256:b0c2d3b226157ae4517d9625decf63591461c66b3a808c2666d538946519d170"
|
||||
"sha256:5c5a2720817414a6c41f0a49993908068243ae02c1635a228126519b509c8aed",
|
||||
"sha256:bf792d480abbd5eda85794e4afb09dd538393f7d6e6ffef6e9f03d2014cf9450"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==3.1.1"
|
||||
"version": "==3.3.0"
|
||||
},
|
||||
"inotify-simple": {
|
||||
"hashes": [
|
||||
@@ -270,11 +277,11 @@
|
||||
},
|
||||
"joblib": {
|
||||
"hashes": [
|
||||
"sha256:698c311779f347cf6b7e6b8a39bb682277b8ee4aba8cf9507bc0cf4cd4737b72",
|
||||
"sha256:9e284edd6be6b71883a63c9b7f124738a3c16195513ad940eae7e3438de885d5"
|
||||
"sha256:75ead23f13484a2a414874779d69ade40d4fa1abe62b222a23cd50d4bc822f6f",
|
||||
"sha256:7ad866067ac1fdec27d51c8678ea760601b70e32ff1881d4dc8e1171f2b64b24"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.17.0"
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"langdetect": {
|
||||
"hashes": [
|
||||
@@ -332,67 +339,60 @@
|
||||
},
|
||||
"numpy": {
|
||||
"hashes": [
|
||||
"sha256:08308c38e44cc926bdfce99498b21eec1f848d24c302519e64203a8da99a97db",
|
||||
"sha256:09c12096d843b90eafd01ea1b3307e78ddd47a55855ad402b157b6c4862197ce",
|
||||
"sha256:13d166f77d6dc02c0a73c1101dd87fdf01339febec1030bd810dcd53fff3b0f1",
|
||||
"sha256:141ec3a3300ab89c7f2b0775289954d193cc8edb621ea05f99db9cb181530512",
|
||||
"sha256:16c1b388cc31a9baa06d91a19366fb99ddbe1c7b205293ed072211ee5bac1ed2",
|
||||
"sha256:18bed2bcb39e3f758296584337966e68d2d5ba6aab7e038688ad53c8f889f757",
|
||||
"sha256:1aeef46a13e51931c0b1cf8ae1168b4a55ecd282e6688fdb0a948cc5a1d5afb9",
|
||||
"sha256:27d3f3b9e3406579a8af3a9f262f5339005dd25e0ecf3cf1559ff8a49ed5cbf2",
|
||||
"sha256:2a2740aa9733d2e5b2dfb33639d98a64c3b0f24765fed86b0fd2aec07f6a0a08",
|
||||
"sha256:4377e10b874e653fe96985c05feed2225c912e328c8a26541f7fc600fb9c637b",
|
||||
"sha256:448ebb1b3bf64c0267d6b09a7cba26b5ae61b6d2dbabff7c91b660c7eccf2bdb",
|
||||
"sha256:50e86c076611212ca62e5a59f518edafe0c0730f7d9195fec718da1a5c2bb1fc",
|
||||
"sha256:5734bdc0342aba9dfc6f04920988140fb41234db42381cf7ccba64169f9fe7ac",
|
||||
"sha256:5ddd1dfa2be066595c1993165b4cae84b9866b12339d0c903db7f21a094324a3",
|
||||
"sha256:64324f64f90a9e4ef732be0928be853eee378fd6a01be21a0a8469c4f2682c83",
|
||||
"sha256:6ae6c680f3ebf1cf7ad1d7748868b39d9f900836df774c453c11c5440bc15b36",
|
||||
"sha256:6d7593a705d662be5bfe24111af14763016765f43cb6923ed86223f965f52387",
|
||||
"sha256:8cac8790a6b1ddf88640a9267ee67b1aee7a57dfa2d2dd33999d080bc8ee3a0f",
|
||||
"sha256:8ece138c3a16db8c1ad38f52eb32be6086cc72f403150a79336eb2045723a1ad",
|
||||
"sha256:9eeb7d1d04b117ac0d38719915ae169aa6b61fca227b0b7d198d43728f0c879c",
|
||||
"sha256:a09f98011236a419ee3f49cedc9ef27d7a1651df07810ae430a6b06576e0b414",
|
||||
"sha256:a5d897c14513590a85774180be713f692df6fa8ecf6483e561a6d47309566f37",
|
||||
"sha256:ad6f2ff5b1989a4899bf89800a671d71b1612e5ff40866d1f4d8bcf48d4e5764",
|
||||
"sha256:c42c4b73121caf0ed6cd795512c9c09c52a7287b04d105d112068c1736d7c753",
|
||||
"sha256:cb1017eec5257e9ac6209ac172058c430e834d5d2bc21961dceeb79d111e5909",
|
||||
"sha256:d6c7bb82883680e168b55b49c70af29b84b84abb161cbac2800e8fcb6f2109b6",
|
||||
"sha256:e452dc66e08a4ce642a961f134814258a082832c78c90351b75c41ad16f79f63",
|
||||
"sha256:e5b6ed0f0b42317050c88022349d994fe72bfe35f5908617512cd8c8ef9da2a9",
|
||||
"sha256:e9b30d4bd69498fc0c3fe9db5f62fffbb06b8eb9321f92cc970f2969be5e3949",
|
||||
"sha256:ec149b90019852266fec2341ce1db513b843e496d5a8e8cdb5ced1923a92faab",
|
||||
"sha256:edb01671b3caae1ca00881686003d16c2209e07b7ef8b7639f1867852b948f7c",
|
||||
"sha256:f0d3929fe88ee1c155129ecd82f981b8856c5d97bcb0d5f23e9b4242e79d1de3",
|
||||
"sha256:f29454410db6ef8126c83bd3c968d143304633d45dc57b51252afbd79d700893",
|
||||
"sha256:fe45becb4c2f72a0907c1d0246ea6449fe7a9e2293bb0e11c4e9a32bb0930a15",
|
||||
"sha256:fedbd128668ead37f33917820b704784aff695e0019309ad446a6d0b065b57e4"
|
||||
"sha256:012426a41bc9ab63bb158635aecccc7610e3eff5d31d1eb43bc099debc979d94",
|
||||
"sha256:06fab248a088e439402141ea04f0fffb203723148f6ee791e9c75b3e9e82f080",
|
||||
"sha256:0eef32ca3132a48e43f6a0f5a82cb508f22ce5a3d6f67a8329c81c8e226d3f6e",
|
||||
"sha256:1ded4fce9cfaaf24e7a0ab51b7a87be9038ea1ace7f34b841fe3b6894c721d1c",
|
||||
"sha256:2e55195bc1c6b705bfd8ad6f288b38b11b1af32f3c8289d6c50d47f950c12e76",
|
||||
"sha256:2ea52bd92ab9f768cc64a4c3ef8f4b2580a17af0a5436f6126b08efbd1838371",
|
||||
"sha256:36674959eed6957e61f11c912f71e78857a8d0604171dfd9ce9ad5cbf41c511c",
|
||||
"sha256:384ec0463d1c2671170901994aeb6dce126de0a95ccc3976c43b0038a37329c2",
|
||||
"sha256:39b70c19ec771805081578cc936bbe95336798b7edf4732ed102e7a43ec5c07a",
|
||||
"sha256:400580cbd3cff6ffa6293df2278c75aef2d58d8d93d3c5614cd67981dae68ceb",
|
||||
"sha256:43d4c81d5ffdff6bae58d66a3cd7f54a7acd9a0e7b18d97abb255defc09e3140",
|
||||
"sha256:50a4a0ad0111cc1b71fa32dedd05fa239f7fb5a43a40663269bb5dc7877cfd28",
|
||||
"sha256:603aa0706be710eea8884af807b1b3bc9fb2e49b9f4da439e76000f3b3c6ff0f",
|
||||
"sha256:6149a185cece5ee78d1d196938b2a8f9d09f5a5ebfbba66969302a778d5ddd1d",
|
||||
"sha256:6373751c4b6fd325606d29dd98dc2bf7092485ad20aafbfc6a177acd3b89059e",
|
||||
"sha256:759e4095edc3c1b3ac031f34d9459fa781777a93ccc633a472a5468587a190ff",
|
||||
"sha256:7fb43004bce0ca31d8f13a6eb5e943fa73371381e53f7074ed21a4cb786c32f8",
|
||||
"sha256:811daee36a58dc79cf3d8bdd4a490e4277d0e4b7d103a001a4e73ddb48e7e6aa",
|
||||
"sha256:8b5e972b43c8fc27d56550b4120fe6257fdc15f9301914380b27f74856299fea",
|
||||
"sha256:99abf4f353c3d1a0c7a5f27699482c987cf663b1eac20db59b8c7b061eabd7fc",
|
||||
"sha256:a0d53e51a6cb6f0d9082decb7a4cb6dfb33055308c4c44f53103c073f649af73",
|
||||
"sha256:a12ff4c8ddfee61f90a1633a4c4afd3f7bcb32b11c52026c92a12e1325922d0d",
|
||||
"sha256:a4646724fba402aa7504cd48b4b50e783296b5e10a524c7a6da62e4a8ac9698d",
|
||||
"sha256:a76f502430dd98d7546e1ea2250a7360c065a5fdea52b2dffe8ae7180909b6f4",
|
||||
"sha256:a9d17f2be3b427fbb2bce61e596cf555d6f8a56c222bd2ca148baeeb5e5c783c",
|
||||
"sha256:ab83f24d5c52d60dbc8cd0528759532736b56db58adaa7b5f1f76ad551416a1e",
|
||||
"sha256:aeb9ed923be74e659984e321f609b9ba54a48354bfd168d21a2b072ed1e833ea",
|
||||
"sha256:c843b3f50d1ab7361ca4f0b3639bf691569493a56808a0b0c54a051d260b7dbd",
|
||||
"sha256:cae865b1cae1ec2663d8ea56ef6ff185bad091a5e33ebbadd98de2cfa3fa668f",
|
||||
"sha256:cc6bd4fd593cb261332568485e20a0712883cf631f6f5e8e86a52caa8b2b50ff",
|
||||
"sha256:cf2402002d3d9f91c8b01e66fbb436a4ed01c6498fffed0e4c7566da1d40ee1e",
|
||||
"sha256:d051ec1c64b85ecc69531e1137bb9751c6830772ee5c1c426dbcfe98ef5788d7",
|
||||
"sha256:d6631f2e867676b13026e2846180e2c13c1e11289d67da08d71cacb2cd93d4aa",
|
||||
"sha256:dbd18bcf4889b720ba13a27ec2f2aac1981bd41203b3a3b27ba7a33f88ae4827",
|
||||
"sha256:df609c82f18c5b9f6cb97271f03315ff0dbe481a2a02e56aeb1b1a985ce38e60"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.19.4"
|
||||
"version": "==1.19.5"
|
||||
},
|
||||
"ocrmypdf": {
|
||||
"hashes": [
|
||||
"sha256:91e7394172cedb3be801a229dbd3d308fb5ae80cbc3a77879fa7954beea407b1",
|
||||
"sha256:e550b8e884150accab7ea41f4a576b5844594cb5cbd6ed514fbf1206720343ad"
|
||||
"sha256:416a9c4321bfc844f250694b8c68ebb538f60609bbc8686bd9f84a13c5127d68",
|
||||
"sha256:f45fc7e844e6026d6080a623a2936be120fc077d99aaa599df022acf35fb31e6"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==11.3.4"
|
||||
},
|
||||
"pathtools": {
|
||||
"hashes": [
|
||||
"sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0",
|
||||
"sha256:d77d982475e87f32b82157a43b09f0a5ef3e66c1d8f3c7eb8d2580e783cd8202"
|
||||
],
|
||||
"version": "==0.1.2"
|
||||
"version": "==11.4.5"
|
||||
},
|
||||
"pathvalidate": {
|
||||
"hashes": [
|
||||
"sha256:1697c8ea71ff4c48e7aa0eda72fe4581404be8f41e51a17363ef682dd6824d35",
|
||||
"sha256:32d30dbacb711c16bb188b12ce7e9a46b41785f50a12f64500f747480a4b6ee3"
|
||||
"sha256:378c8b319838a255c00ab37f664686b75f0aabea4444d6c5a34effbec6738285",
|
||||
"sha256:cae8ad5cd9223c5c1f4bc4e2ef0cd4c5e89acd2d698fdb7610ee108b9be654d2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.3.0"
|
||||
"version": "==2.3.2"
|
||||
},
|
||||
"pdfminer.six": {
|
||||
"hashes": [
|
||||
@@ -411,65 +411,65 @@
|
||||
},
|
||||
"pikepdf": {
|
||||
"hashes": [
|
||||
"sha256:0829bd5dacd73bb4a37e7575bae523f49603479755563c92ddb55c206700cab1",
|
||||
"sha256:0d2b631077cd6af6e4d1b396208020705842610a6f13fab489d5f9c47916baa2",
|
||||
"sha256:21c98af08fae4ac9fbcad02b613b6768a4ca300fda4cba867f4a4b6f73c2d04b",
|
||||
"sha256:2240372fed30124ddc35b0c15a613f2b687a426ea2f150091e0a0c58cca7a495",
|
||||
"sha256:2a97f5f1403e058d217d7f6861cf51fca200c5687bce0d052f5f2fa89b5bfa22",
|
||||
"sha256:3faaefca0ae80d19891acec8b0dd5e6235f59f2206d82375eb80d090285e9557",
|
||||
"sha256:48ef45b64882901c0d69af3b85d16a19bd0f3e95b43e614fefb53521d8caf36c",
|
||||
"sha256:5212fe41f2323fc7356ba67caa39737fe13080562cff37bcbb74a8094076c8d0",
|
||||
"sha256:56859c32170663c57bd0658189ce44e180533eebe813853446cd6413810be9eb",
|
||||
"sha256:5f8fd1cb3478c5534222018aca24fbbd2bc74460c899bda988ec76722c13caa9",
|
||||
"sha256:74300a32c41b3d578772f6933f23a88b19f74484185e71e5225ce2f7ea5aea78",
|
||||
"sha256:8cbc946bdd217148f4a9c029fcea62f4ae0f67d5346de4c865f4718cd0ddc37f",
|
||||
"sha256:9ceefd30076f732530cf84a1be2ecb2fa9931af932706ded760a6d37c73b96ad",
|
||||
"sha256:ad69c170fda41b07a4c6b668a3128e7a759f50d9aebcfcde0ccff1358abe0423",
|
||||
"sha256:b715fe182189fb6870fab5b0383bb2fb278c88c46eade346b0f4c1ed8818c09d",
|
||||
"sha256:bb01ecf95083ffcb9ad542dc5342ccc1059e46f1395fd966629d36d9cc766b4a",
|
||||
"sha256:bd6328547219cf48cefb4e0a1bc54442910594de1c5a5feae847d9ff3c629031",
|
||||
"sha256:edb128379bb1dea76b5bdbdacf5657a6e4754bacc2049640762725590d8ed905",
|
||||
"sha256:f8e687900557fcd4c51b4e72b9e337fdae9e2c81049d1d80b624bb2e88b5769d",
|
||||
"sha256:fe0ca120e3347c851c34a91041d574f3c588d832023906d8ae18d66d042e8a52",
|
||||
"sha256:fe8e0152672f24d8bfdecc725f97e9013f2de1b41849150959526ca3562bd3ef"
|
||||
"sha256:0e67e5beeeed5422b3b8e862e4777fed5a4cd3c72e711e2a449a65d9ee641448",
|
||||
"sha256:138155ae1f71634cd6eca79f5517f77b2067ef0bd5b627ea9414e308fe868dc5",
|
||||
"sha256:15cf648dd760a47c55a4106b601b92bb653ae98155b10f04310553629c6695dd",
|
||||
"sha256:1d6a011ae4c501c78509caf19cbe152c2e3cb5c267f7b47bc3db8cd3436585a7",
|
||||
"sha256:211f529313953e44ae42eb896c2b688668385e6e8f9d04d21484bddb3c42b34c",
|
||||
"sha256:22049ad288d603a7fc68e90a0722770d307886788373ddfe71fbf614ced0f5b2",
|
||||
"sha256:24f7c371f6ecbee8f0ae30030992fc75cd32cd575dcfca8d466a03a8290377ca",
|
||||
"sha256:26cdf561632866d584fedb6b1c1fce78cefa49b5cae54c65aa6a6ca5fe6de4ac",
|
||||
"sha256:2c37afcd21a2eb1da1773687e853327fa8ec7d2c5cd90cdcd70180f55f0221e1",
|
||||
"sha256:65b8ec6403814f51e1b9c7e18a8ff26087fcc7a199b1405583e5ff9eb931db56",
|
||||
"sha256:66a03103aadb2e2738271cb18c89837ac3980fa0b4687195c4c150228b7e79de",
|
||||
"sha256:6e8f0124354c53a66f83ec5a18111b760aeff1a64db3a86e7ee5fed8e8624707",
|
||||
"sha256:70f2836cd468aa25bc8b09a2b9561364bd75d3e6ddb0e50a25d248d7da6cff25",
|
||||
"sha256:82cebf68952cfb65c86d880eb782a0c558b37531cdae59f2e11fcd0f2bb4669c",
|
||||
"sha256:84ad3e8fd5f3251fb5b534614da64b04a264ce9348f0fe35b781c0fb378b0f82",
|
||||
"sha256:af13fbc022efa85d1ae161129d4cde66493479db52b9adb74d525b890a078208",
|
||||
"sha256:c1d40fb8f8192c75f54f0e74a569ccf45e4e13bed8da78a78a5b488be29979bf",
|
||||
"sha256:d147ec1ab58512871fdf40a161809f698eaa75720b4a230198e7e028582b20a1",
|
||||
"sha256:dedad1f68d6b0b54000f7f99386351f1c6e19c8cf70a9700d8dd06b9809c54fb",
|
||||
"sha256:e72c3f5b624b9c7341fd6a7e657926d4cf12a7ea453681ffd7332cabc3530c62",
|
||||
"sha256:eb75f22e261b3bc69b6fc9a17b1d6966c95e79d3e792b7737a018a2bf6a2b07f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.2.0"
|
||||
"version": "==2.2.5"
|
||||
},
|
||||
"pillow": {
|
||||
"hashes": [
|
||||
"sha256:006de60d7580d81f4a1a7e9f0173dc90a932e3905cc4d47ea909bc946302311a",
|
||||
"sha256:0a2e8d03787ec7ad71dc18aec9367c946ef8ef50e1e78c71f743bc3a770f9fae",
|
||||
"sha256:0eeeae397e5a79dc088d8297a4c2c6f901f8fb30db47795113a4a605d0f1e5ce",
|
||||
"sha256:11c5c6e9b02c9dac08af04f093eb5a2f84857df70a7d4a6a6ad461aca803fb9e",
|
||||
"sha256:2fb113757a369a6cdb189f8df3226e995acfed0a8919a72416626af1a0a71140",
|
||||
"sha256:4b0ef2470c4979e345e4e0cc1bbac65fda11d0d7b789dbac035e4c6ce3f98adb",
|
||||
"sha256:59e903ca800c8cfd1ebe482349ec7c35687b95e98cefae213e271c8c7fffa021",
|
||||
"sha256:5a3342d34289715928c914ee7f389351eb37fa4857caa9297fc7948f2ed3e53d",
|
||||
"sha256:5abd653a23c35d980b332bc0431d39663b1709d64142e3652890df4c9b6970f6",
|
||||
"sha256:5f9403af9c790cc18411ea398a6950ee2def2a830ad0cfe6dc9122e6d528b302",
|
||||
"sha256:6b4a8fd632b4ebee28282a9fef4c341835a1aa8671e2770b6f89adc8e8c2703c",
|
||||
"sha256:6c1aca8231625115104a06e4389fcd9ec88f0c9befbabd80dc206c35561be271",
|
||||
"sha256:795e91a60f291e75de2e20e6bdd67770f793c8605b553cb6e4387ce0cb302e09",
|
||||
"sha256:7ba0ba61252ab23052e642abdb17fd08fdcfdbbf3b74c969a30c58ac1ade7cd3",
|
||||
"sha256:7c9401e68730d6c4245b8e361d3d13e1035cbc94db86b49dc7da8bec235d0015",
|
||||
"sha256:81f812d8f5e8a09b246515fac141e9d10113229bc33ea073fec11403b016bcf3",
|
||||
"sha256:895d54c0ddc78a478c80f9c438579ac15f3e27bf442c2a9aa74d41d0e4d12544",
|
||||
"sha256:8de332053707c80963b589b22f8e0229f1be1f3ca862a932c1bcd48dafb18dd8",
|
||||
"sha256:92c882b70a40c79de9f5294dc99390671e07fc0b0113d472cbea3fde15db1792",
|
||||
"sha256:95edb1ed513e68bddc2aee3de66ceaf743590bf16c023fb9977adc4be15bd3f0",
|
||||
"sha256:b63d4ff734263ae4ce6593798bcfee6dbfb00523c82753a3a03cbc05555a9cc3",
|
||||
"sha256:bd7bf289e05470b1bc74889d1466d9ad4a56d201f24397557b6f65c24a6844b8",
|
||||
"sha256:cc3ea6b23954da84dbee8025c616040d9aa5eaf34ea6895a0a762ee9d3e12e11",
|
||||
"sha256:cc9ec588c6ef3a1325fa032ec14d97b7309db493782ea8c304666fb10c3bd9a7",
|
||||
"sha256:d3d07c86d4efa1facdf32aa878bd508c0dc4f87c48125cc16b937baa4e5b5e11",
|
||||
"sha256:d8a96747df78cda35980905bf26e72960cba6d355ace4780d4bdde3b217cdf1e",
|
||||
"sha256:e38d58d9138ef972fceb7aeec4be02e3f01d383723965bfcef14d174c8ccd039",
|
||||
"sha256:eb472586374dc66b31e36e14720747595c2b265ae962987261f044e5cce644b5",
|
||||
"sha256:fbd922f702582cb0d71ef94442bfca57624352622d75e3be7a1e7e9360b07e72"
|
||||
"sha256:165c88bc9d8dba670110c689e3cc5c71dbe4bfb984ffa7cbebf1fac9554071d6",
|
||||
"sha256:22d070ca2e60c99929ef274cfced04294d2368193e935c5d6febfd8b601bf865",
|
||||
"sha256:2353834b2c49b95e1313fb34edf18fca4d57446675d05298bb694bca4b194174",
|
||||
"sha256:39725acf2d2e9c17356e6835dccebe7a697db55f25a09207e38b835d5e1bc032",
|
||||
"sha256:3de6b2ee4f78c6b3d89d184ade5d8fa68af0848f9b6b6da2b9ab7943ec46971a",
|
||||
"sha256:47c0d93ee9c8b181f353dbead6530b26980fe4f5485aa18be8f1fd3c3cbc685e",
|
||||
"sha256:5e2fe3bb2363b862671eba632537cd3a823847db4d98be95690b7e382f3d6378",
|
||||
"sha256:604815c55fd92e735f9738f65dabf4edc3e79f88541c221d292faec1904a4b17",
|
||||
"sha256:6c5275bd82711cd3dcd0af8ce0bb99113ae8911fc2952805f1d012de7d600a4c",
|
||||
"sha256:731ca5aabe9085160cf68b2dbef95fc1991015bc0a3a6ea46a371ab88f3d0913",
|
||||
"sha256:7612520e5e1a371d77e1d1ca3a3ee6227eef00d0a9cddb4ef7ecb0b7396eddf7",
|
||||
"sha256:7916cbc94f1c6b1301ac04510d0881b9e9feb20ae34094d3615a8a7c3db0dcc0",
|
||||
"sha256:81c3fa9a75d9f1afafdb916d5995633f319db09bd773cb56b8e39f1e98d90820",
|
||||
"sha256:887668e792b7edbfb1d3c9d8b5d8c859269a0f0eba4dda562adb95500f60dbba",
|
||||
"sha256:8c183b5c60544b49e0a66f924b18c526dfd37774811b627f70836fe01711abd3",
|
||||
"sha256:93a473b53cc6e0b3ce6bf51b1b95b7b1e7e6084be3a07e40f79b42e83503fbf2",
|
||||
"sha256:96d4dc103d1a0fa6d47c6c55a47de5f5dafd5ef0114fa10c85a1fd8e0216284b",
|
||||
"sha256:a3d3e086474ef12ef13d42e5f9b7bbf09d39cf6bd4940f982263d6954b13f6a9",
|
||||
"sha256:b02a0b9f332086657852b1f7cb380f6a42403a6d9c42a4c34a561aa4530d5234",
|
||||
"sha256:b09e10ec453de97f9a23a5aa5e30b334195e8d2ddd1ce76cc32e52ba63c8b31d",
|
||||
"sha256:b6f00ad5ebe846cc91763b1d0c6d30a8042e02b2316e27b05de04fa6ec831ec5",
|
||||
"sha256:bba80df38cfc17f490ec651c73bb37cd896bc2400cfba27d078c2135223c1206",
|
||||
"sha256:c3d911614b008e8a576b8e5303e3db29224b455d3d66d1b2848ba6ca83f9ece9",
|
||||
"sha256:ca20739e303254287138234485579b28cb0d524401f83d5129b5ff9d606cb0a8",
|
||||
"sha256:cb192176b477d49b0a327b2a5a4979552b7a58cd42037034316b8018ac3ebb59",
|
||||
"sha256:cdbbe7dff4a677fb555a54f9bc0450f2a21a93c5ba2b44e09e54fcb72d2bd13d",
|
||||
"sha256:d355502dce85ade85a2511b40b4c61a128902f246504f7de29bbeec1ae27933a",
|
||||
"sha256:dc577f4cfdda354db3ae37a572428a90ffdbe4e51eda7849bf442fb803f09c9b",
|
||||
"sha256:dd9eef866c70d2cbbea1ae58134eaffda0d4bfea403025f4db6859724b18ab3d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==8.0.1"
|
||||
"version": "==8.1.0"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
@@ -574,10 +574,10 @@
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
"sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268",
|
||||
"sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd"
|
||||
"sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4",
|
||||
"sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5"
|
||||
],
|
||||
"version": "==2020.4"
|
||||
"version": "==2020.5"
|
||||
},
|
||||
"redis": {
|
||||
"hashes": [
|
||||
@@ -638,50 +638,58 @@
|
||||
},
|
||||
"reportlab": {
|
||||
"hashes": [
|
||||
"sha256:0008b5baa39d7e3a8132c4b47ecae88d6858ad386518e754e5e7b8025ee4722b",
|
||||
"sha256:0ad5a540c336941272fe161ef3a9830da3d4b3a65a195531cebd3cad5db58b2a",
|
||||
"sha256:0c965a5691686d746f558ee1c52aa9c63a01a0e13cba61ffc661573948e32f61",
|
||||
"sha256:0fd568fa5615ae99f76289c52ff230207852ee942d4934f6c893c93d2a79544e",
|
||||
"sha256:1117d905a3404c696869c7aabec9454b43ed6acbbc73f9256c6fcea23e7ae93e",
|
||||
"sha256:1ea7c388e91ad9d823655ad6a13751ff67e8a0e7cf4065cf051b4c931cdd9450",
|
||||
"sha256:26c0ee8f62652cc7fcdc47a1cb3b34775a4d625738025c1a7edb8718bda5a315",
|
||||
"sha256:368c5b3fc3d5a541cb9dcacefa563fdb445365f517e3cbf64b4326631d1cf13c",
|
||||
"sha256:451d42fdcdd7d84587d6d9c8f5d9a7d0e997305efb606705063ca1fe8bcca551",
|
||||
"sha256:47394acba4da8e56ef8e55d8eb483b868521696ba49ab0f0fcf8a1a4a5ac6e49",
|
||||
"sha256:51b16e297f7b937fc530dd151e4b38f1d305b01c9aa10657bc32a5d2901b8ad7",
|
||||
"sha256:51c0cdcf606ded0a7b4b50050400f25125ea797fbfc3c817135993b38f8b764e",
|
||||
"sha256:55c672c579618843e0fd00140fb71f1ffebc4f1c542ac385c4f4999f2f5398d9",
|
||||
"sha256:5c34a96ecfbf595caf16178a06abcd26a5f8720e01fe1285d4c97333382cfaeb",
|
||||
"sha256:61aa89a00754b18c4f2956b8bff831f1fd3affef6476dc63462d92211941605e",
|
||||
"sha256:62234d29c97279917903e4587faf240a5dea4617be250db55386ff268eb5a7c5",
|
||||
"sha256:670f2a8dcc23bf798c39b95c64bf76ee387549b962f76783670821978a226663",
|
||||
"sha256:69387f171f6c7b55109caa6d061b17a18f2f9e724a0212c07cd692aeb369dd19",
|
||||
"sha256:6c5c8871b659f7c2975382d7b61f3c182701fa9eb62cf649c3c73ba8fc5e2595",
|
||||
"sha256:80139ceb3a568f5be908094f1701fd05391b71425e8b69aaed0d30db647ca2aa",
|
||||
"sha256:80661a76d0019b5e2c315ccd3bc7093d754067d6142b36a3a0ec4f416073d23b",
|
||||
"sha256:85a2236f324ae336da7f4b183fa99bed261bcc00ac1255ee91a504e68b086d00",
|
||||
"sha256:89a3acd98bd4478d6bbc5cb32e0665ea546c98bff8b58d5e1014659daa6ef75a",
|
||||
"sha256:8a39119fcab146bde41fd1c6d148f9ee1e2cca10c6f9c2b7eb4dd710a3a2c6ac",
|
||||
"sha256:9c31c2526401da6cc92018f68483f2aac0a731cb98435445ea4b72d46b438c84",
|
||||
"sha256:9e8ae1c3b8a1697147c5c97f00d66ab1c54d88c4615b0cdd9b1a667d7baf3eb7",
|
||||
"sha256:a479c38ab2b997ce05d3bef906783ac20cf4cb224a154e80c9018c5e4d943a35",
|
||||
"sha256:a79aab8d069543d5085d58260f18705a08acd92a4501a41261913fddc2137d46",
|
||||
"sha256:b0a8314383de853599ca531dfe55eaa49bb8d6b0bb663b2f8479b7a0f3385ea2",
|
||||
"sha256:b3d9926e64bd8008007b2d9819d7b30179b069ce95431d5060f71afc36885389",
|
||||
"sha256:c2a9a77ce4f25ffb52d705be82a9f41b47f6b0da23870ebc3587709e7242da30",
|
||||
"sha256:c578dd0799f70fb577474cd383f035c6e1057e4fe837278113f9cfa6eee4b076",
|
||||
"sha256:c5abd9d0023ad20030524ab0d5fa39d77aed025519b1fa426304ab2dd0328b89",
|
||||
"sha256:ced96125525ba21311e9512adf391170b9e149f89e27e45b06ff07b70f97a0b2",
|
||||
"sha256:d692fb88d6ef5e75242b00009b54953a0425eaa8bd3a36db9db8b396785e1f57",
|
||||
"sha256:d70c2104286459658e61388af9eee838b612986bd8a36e1d21ba36152983ac15",
|
||||
"sha256:de47c65c10ac6f0d2addb28f1b1657b1c707aca014d09d01b3b728cf19e8f791",
|
||||
"sha256:e6e7592527791841db0820a72c6afae52655a05b0b6d4df184fd2bafe82ee1ee",
|
||||
"sha256:e8a7e95ee6ea5566291b59ede5b9fadce809dca43ebfbfe11e3ff3d6492c6f0e",
|
||||
"sha256:f041759138b3a95508c4281b3db3bf9bb28636d84c554272a58a5ca7c9f9bbf4",
|
||||
"sha256:f39c7fc1fa2e4a1d9747a3effd70731a9d0e9eb5738247fa089c059eff19d43e",
|
||||
"sha256:f65ac89ee0ba569f5279360eae08783f7f2e95c9810a9846c957fbd5950f4896"
|
||||
"sha256:009fa61710647cdc62eb373345248d8ebb93583a058990f7c4f9be46d90aa5b1",
|
||||
"sha256:04a08d284da86882ec3a41a7c719833362ef891b09ee8e2fbb47cee352aa684a",
|
||||
"sha256:07bff6742fba612da8d1b1f783c436338c6fdc6962828159827d5ca7d2b67935",
|
||||
"sha256:09fb11ab1500e679fc1b01199d2fed24435499856e75043a9ac0d31dd48fd881",
|
||||
"sha256:18a876449c9000c391dd3415ebc8454cd7bb9e488977b894886a2d7d018f16cd",
|
||||
"sha256:18eec161411026dde49767bee4e5e8eeb8014879554811a62581dc7433628d5b",
|
||||
"sha256:19353aead39fc115a4d6c598d6fb9fa26da7e69160a0443ebb49b02903e704e8",
|
||||
"sha256:1b85c20e89c22ae902ca973df2afdd2d64d27dc4ffd2b29ebad8c805a213756b",
|
||||
"sha256:1da3d7a35f918cee905facfa94bd00ae6091cadc06dca1b0b31b69ae02d41d1d",
|
||||
"sha256:1e484ce83dae26cb40fcbd312d45b8ba921de7856a00339d867dd4ecf145a1e7",
|
||||
"sha256:33f3cfdc492575f8af3225701301a7e62fc478358729820c9e0091aff5831378",
|
||||
"sha256:3b0026c1129147befd4e5a8cf25da8dea1096fce371e7b2412e36d7254019c06",
|
||||
"sha256:3d7713dddaa8081ed709a1fa2456a43f6a74b0f07d605da8441fd53fef334f69",
|
||||
"sha256:3e2b4d69763103b9dc9b54c0952dc3cee05cedd06e28c0987fad7f84705b12c0",
|
||||
"sha256:4ca5233a19a5ceca23546290f43addec2345789c7d65bb32f8b2668aa148351f",
|
||||
"sha256:5214a289cf01ebbd65e49bae83709671dd9edb601891cf0ae8abf85f3c0b392f",
|
||||
"sha256:52f8237654acbc78ea2fa6fb4a6a06e5b023b6da93f7889adfe2deba09473fad",
|
||||
"sha256:5ed00894e0f8281c0b7c0494b4d3067c641fd90c8e5cf933089ec4cc9a48e491",
|
||||
"sha256:6191961533d49c9d860964d42bada4d7ac3bb28502d984feb8034093f2012fa8",
|
||||
"sha256:6f3ad2b1afe99c436563cd436d8693d4a12e2c4bd45f70c7705759ff7837fe53",
|
||||
"sha256:739b743b7ca1ba4b4d64c321de6fccb49b562d0507ea06c817d9cc4faed5cd22",
|
||||
"sha256:792efba0c0c6e4ee94f6dc95f305451733ee9230a1c7d51cb8e5301a549e0dfb",
|
||||
"sha256:79d63ca40231ca3860859b39a92daa5219035ba9553da89a5e1b218550744121",
|
||||
"sha256:83b28104edd58ad65748d2d0e60e0d97e3b91b3e90b4573ea6fe60de6811972c",
|
||||
"sha256:85650446538cd2f606ca234634142a7ccd74cb6db7cfec250f76a4242e0f2431",
|
||||
"sha256:8850eba6de6eb813036eb8dce353e40d60c8af48bbce107de82770b10d3aa525",
|
||||
"sha256:9da445cb79e3f740756924c053edc952cde11a65ff5af8acfda3c0a1317136ef",
|
||||
"sha256:9fabd5fbd24f5971085ffe53150d663f158f7d3050b25c95736e29ebf676d454",
|
||||
"sha256:a0c377bc45e73c3f15f55d7de69fab270d174749d5b454ab0de502b15430ec2a",
|
||||
"sha256:a1d3f7022a920d4a5e165d264581f1862e1c1b877ceeabb96fe98cec98125ae5",
|
||||
"sha256:a315edef5c5610b0c75790142f49487e89ea34397fc247ae8aa890fe6d6dd057",
|
||||
"sha256:a755cca2dcf023130b03bb671670301a992157d5c3151d838c0b68ef89894536",
|
||||
"sha256:b1b20208ecdfffd7ca027955c4fe8972b28b30a4b3b80cf25099a08d3b20ed7c",
|
||||
"sha256:b26d6f416891cef93411d6d478a25db275766081a5fb66368248293ef459f3be",
|
||||
"sha256:b4ba4c30af7044ee987e61c88a5ffb76031ca0c53666bc85d823b7de55ddbc75",
|
||||
"sha256:b71faf3b6e4d7058e1af1b8afedaf39a962db4a219affc8177009d8244ec10d4",
|
||||
"sha256:cfa854bea525f8c913cb77e2bda724d94b965a0eb3bcfc4a645a9baa29bb86e2",
|
||||
"sha256:dd9687359e466086b9f6fe6d8069034017f8b6ca3080944fae5709767ca6814e",
|
||||
"sha256:de0c675fc2998a7eaa929c356ba49c84f53a892e9ab25e8ee7d8ebbbdcb2ac16",
|
||||
"sha256:e2b4e33fea2ce9d3a14ea39191b169e41eb2ac995274f54ac8fd27519974bce8",
|
||||
"sha256:f3d4a1a273dc141e03b72a553c11bc14dd7a27ec7654a071edcf83eb04f004bc",
|
||||
"sha256:ff547cf4c1de7e104cad1a378431ff81efcb03e90e40871ee686107da5b91442"
|
||||
],
|
||||
"version": "==3.5.56"
|
||||
"version": "==3.5.59"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
|
||||
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==2.25.1"
|
||||
},
|
||||
"scikit-learn": {
|
||||
"hashes": [
|
||||
@@ -769,13 +777,30 @@
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==2.1.0"
|
||||
},
|
||||
"tqdm": {
|
||||
"tika": {
|
||||
"hashes": [
|
||||
"sha256:38b658a3e4ecf9b4f6f8ff75ca16221ae3378b2e175d846b6b33ea3a20852cf5",
|
||||
"sha256:d4f413aecb61c9779888c64ddf0c62910ad56dcbe857d8922bb505d4dbff0df1"
|
||||
"sha256:c2c50f405622f74531841104f9e85c17511aede11de8e5385eab1a29a31f191b",
|
||||
"sha256:d1f2eddb93caa9a2857569486aa2bc0320d0bf1796cdbe03066954cbc4b4bf62"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.54.1"
|
||||
"version": "==1.24"
|
||||
},
|
||||
"tqdm": {
|
||||
"hashes": [
|
||||
"sha256:556c55b081bd9aa746d34125d024b73f0e2a0e62d5927ff0e400e20ee0a03b9a",
|
||||
"sha256:b8b46036fd00176d0870307123ef06bb851096964fa7fc578d789f90ce82c3e4"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.55.1"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
|
||||
"sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
|
||||
"sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==3.7.4.3"
|
||||
},
|
||||
"tzlocal": {
|
||||
"hashes": [
|
||||
@@ -784,13 +809,36 @@
|
||||
],
|
||||
"version": "==2.1"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08",
|
||||
"sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
|
||||
"version": "==1.26.2"
|
||||
},
|
||||
"watchdog": {
|
||||
"hashes": [
|
||||
"sha256:3caefdcc8f06a57fdc5ef2d22aa7c0bfda4f55e71a0bee74cbf3176d97536ef3",
|
||||
"sha256:e38bffc89b15bafe2a131f0e1c74924cf07dcec020c2e0a26cccd208831fcd43"
|
||||
"sha256:016b01495b9c55b5d4126ed8ae75d93ea0d99377084107c33162df52887cee18",
|
||||
"sha256:101532b8db506559e52a9b5d75a308729b3f68264d930670e6155c976d0e52a0",
|
||||
"sha256:27d9b4666938d5d40afdcdf2c751781e9ce36320788b70208d0f87f7401caf93",
|
||||
"sha256:2f1ade0d0802503fda4340374d333408831cff23da66d7e711e279ba50fe6c4a",
|
||||
"sha256:376cbc2a35c0392b0fe7ff16fbc1b303fd99d4dd9911ab5581ee9d69adc88982",
|
||||
"sha256:57f05e55aa603c3b053eed7e679f0a83873c540255b88d58c6223c7493833bac",
|
||||
"sha256:5f1f3b65142175366ba94c64d8d4c8f4015825e0beaacee1c301823266b47b9b",
|
||||
"sha256:602dbd9498592eacc42e0632c19781c3df1728ef9cbab555fab6778effc29eeb",
|
||||
"sha256:68744de2003a5ea2dfbb104f9a74192cf381334a9e2c0ed2bbe1581828d50b61",
|
||||
"sha256:85e6574395aa6c1e14e0f030d9d7f35c2340a6cf95d5671354ce876ac3ffdd4d",
|
||||
"sha256:b1d723852ce90a14abf0ec0ca9e80689d9509ee4c9ee27163118d87b564a12ac",
|
||||
"sha256:d948ad9ab9aba705f9836625b32e965b9ae607284811cd98334423f659ea537a",
|
||||
"sha256:e2a531e71be7b5cc3499ae2d1494d51b6a26684bcc7c3146f63c810c00e8a3cc",
|
||||
"sha256:e7c73edef48f4ceeebb987317a67e0080e5c9228601ff67b3c4062fa020403c7",
|
||||
"sha256:ee21aeebe6b3e51e4ba64564c94cee8dbe7438b9cb60f0bb350c4fa70d1b52c2",
|
||||
"sha256:f1d0e878fd69129d0d68b87cee5d9543f20d8018e82998efb79f7e412d42154a",
|
||||
"sha256:f84146f7864339c8addf2c2b9903271df21d18d2c721e9a77f779493234a82b5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.10.4"
|
||||
"version": "==1.0.2"
|
||||
},
|
||||
"wcwidth": {
|
||||
"hashes": [
|
||||
@@ -873,53 +921,68 @@
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
|
||||
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
|
||||
],
|
||||
"markers": "python_version >= '3.1'",
|
||||
"version": "==3.0.4"
|
||||
"version": "==4.0.0"
|
||||
},
|
||||
"coverage": {
|
||||
"hashes": [
|
||||
"sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516",
|
||||
"sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259",
|
||||
"sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9",
|
||||
"sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097",
|
||||
"sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0",
|
||||
"sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f",
|
||||
"sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7",
|
||||
"sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c",
|
||||
"sha256:3188a7dfd96f734a7498f37cde6598b1e9c084f1ca68bc1aa04e88db31168ab6",
|
||||
"sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5",
|
||||
"sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7",
|
||||
"sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729",
|
||||
"sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978",
|
||||
"sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9",
|
||||
"sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f",
|
||||
"sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9",
|
||||
"sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822",
|
||||
"sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418",
|
||||
"sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82",
|
||||
"sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f",
|
||||
"sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d",
|
||||
"sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221",
|
||||
"sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4",
|
||||
"sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21",
|
||||
"sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709",
|
||||
"sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54",
|
||||
"sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d",
|
||||
"sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270",
|
||||
"sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24",
|
||||
"sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751",
|
||||
"sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a",
|
||||
"sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237",
|
||||
"sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7",
|
||||
"sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636",
|
||||
"sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8",
|
||||
"sha256:ef221855191457fffeb909d5787d1807800ab4d0111f089e6c93ee68f577634d"
|
||||
"sha256:08b3ba72bd981531fd557f67beee376d6700fba183b167857038997ba30dd297",
|
||||
"sha256:262066798d786ad67a13c7ba869e3ce0e39609f99f6d6c80160ad602c4808e32",
|
||||
"sha256:2757fa64e11ec12220968f65d086b7a29b6583d16e9a544c889b22ba98555ef1",
|
||||
"sha256:3102bb2c206700a7d28181dbe04d66b30780cde1d1c02c5f3c165cf3d2489497",
|
||||
"sha256:3498b27d8236057def41de3585f317abae235dd3a11d33e01736ffedb2ef8606",
|
||||
"sha256:378ac77af41350a8c6b8801a66021b52da8a05fd77e578b7380e876c0ce4f528",
|
||||
"sha256:38f16b1317b8dd82df67ed5daa5f5e7c959e46579840d77a67a4ceb9cef0a50b",
|
||||
"sha256:3911c2ef96e5ddc748a3c8b4702c61986628bb719b8378bf1e4a6184bbd48fe4",
|
||||
"sha256:3a3c3f8863255f3c31db3889f8055989527173ef6192a283eb6f4db3c579d830",
|
||||
"sha256:3b14b1da110ea50c8bcbadc3b82c3933974dbeea1832e814aab93ca1163cd4c1",
|
||||
"sha256:535dc1e6e68fad5355f9984d5637c33badbdc987b0c0d303ee95a6c979c9516f",
|
||||
"sha256:6f61319e33222591f885c598e3e24f6a4be3533c1d70c19e0dc59e83a71ce27d",
|
||||
"sha256:723d22d324e7997a651478e9c5a3120a0ecbc9a7e94071f7e1954562a8806cf3",
|
||||
"sha256:76b2775dda7e78680d688daabcb485dc87cf5e3184a0b3e012e1d40e38527cc8",
|
||||
"sha256:782a5c7df9f91979a7a21792e09b34a658058896628217ae6362088b123c8500",
|
||||
"sha256:7e4d159021c2029b958b2363abec4a11db0ce8cd43abb0d9ce44284cb97217e7",
|
||||
"sha256:8dacc4073c359f40fcf73aede8428c35f84639baad7e1b46fce5ab7a8a7be4bb",
|
||||
"sha256:8f33d1156241c43755137288dea619105477961cfa7e47f48dbf96bc2c30720b",
|
||||
"sha256:8ffd4b204d7de77b5dd558cdff986a8274796a1e57813ed005b33fd97e29f059",
|
||||
"sha256:93a280c9eb736a0dcca19296f3c30c720cb41a71b1f9e617f341f0a8e791a69b",
|
||||
"sha256:9a4f66259bdd6964d8cf26142733c81fb562252db74ea367d9beb4f815478e72",
|
||||
"sha256:9a9d4ff06804920388aab69c5ea8a77525cf165356db70131616acd269e19b36",
|
||||
"sha256:a2070c5affdb3a5e751f24208c5c4f3d5f008fa04d28731416e023c93b275277",
|
||||
"sha256:a4857f7e2bc6921dbd487c5c88b84f5633de3e7d416c4dc0bb70256775551a6c",
|
||||
"sha256:a607ae05b6c96057ba86c811d9c43423f35e03874ffb03fbdcd45e0637e8b631",
|
||||
"sha256:a66ca3bdf21c653e47f726ca57f46ba7fc1f260ad99ba783acc3e58e3ebdb9ff",
|
||||
"sha256:ab110c48bc3d97b4d19af41865e14531f300b482da21783fdaacd159251890e8",
|
||||
"sha256:b239711e774c8eb910e9b1ac719f02f5ae4bf35fa0420f438cdc3a7e4e7dd6ec",
|
||||
"sha256:be0416074d7f253865bb67630cf7210cbc14eb05f4099cc0f82430135aaa7a3b",
|
||||
"sha256:c46643970dff9f5c976c6512fd35768c4a3819f01f61169d8cdac3f9290903b7",
|
||||
"sha256:c5ec71fd4a43b6d84ddb88c1df94572479d9a26ef3f150cef3dacefecf888105",
|
||||
"sha256:c6e5174f8ca585755988bc278c8bb5d02d9dc2e971591ef4a1baabdf2d99589b",
|
||||
"sha256:c89b558f8a9a5a6f2cfc923c304d49f0ce629c3bd85cb442ca258ec20366394c",
|
||||
"sha256:cc44e3545d908ecf3e5773266c487ad1877be718d9dc65fc7eb6e7d14960985b",
|
||||
"sha256:cc6f8246e74dd210d7e2b56c76ceaba1cc52b025cd75dbe96eb48791e0250e98",
|
||||
"sha256:cd556c79ad665faeae28020a0ab3bda6cd47d94bec48e36970719b0b86e4dcf4",
|
||||
"sha256:ce6f3a147b4b1a8b09aae48517ae91139b1b010c5f36423fa2b866a8b23df879",
|
||||
"sha256:ceb499d2b3d1d7b7ba23abe8bf26df5f06ba8c71127f188333dddcf356b4b63f",
|
||||
"sha256:cef06fb382557f66d81d804230c11ab292d94b840b3cb7bf4450778377b592f4",
|
||||
"sha256:e448f56cfeae7b1b3b5bcd99bb377cde7c4eb1970a525c770720a352bc4c8044",
|
||||
"sha256:e52d3d95df81c8f6b2a1685aabffadf2d2d9ad97203a40f8d61e51b70f191e4e",
|
||||
"sha256:eb33c4c858d06bd8d79713c7628d3f2b50fb1c62071e2e88cb44876be03eabe1",
|
||||
"sha256:ee2f1d1c223c3d2c24e3afbb2dd38be3f03b1a8d6a83ee3d9eb8c36a52bee899",
|
||||
"sha256:f2c6888eada180814b8583c3e793f3f343a692fc802546eed45f40a001b1169f",
|
||||
"sha256:f51dbba78d68a44e99d484ca8c8f604f17e957c1ca09c3ebc2c7e3bbd9ba0448",
|
||||
"sha256:f54de00baf200b4539a5a092a759f000b5f45fd226d6d25a76b0dff71177a714",
|
||||
"sha256:fa10fee7e32213f5c7b0d6428ea92e3a3fdd6d725590238a3f92c0de1c78b9d2",
|
||||
"sha256:fabeeb121735d47d8eab8671b6b031ce08514c86b7ad8f7d5490a7b6dcd6267d",
|
||||
"sha256:fac3c432851038b3e6afe086f777732bcf7f6ebbfd90951fa04ee53db6d0bcdd",
|
||||
"sha256:fda29412a66099af6d6de0baa6bd7c52674de177ec2ad2630ca264142d69c6c7",
|
||||
"sha256:ff1330e8bc996570221b450e2d539134baa9465f5cb98aff0e0f73f34172e0ae"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
|
||||
"version": "==5.3"
|
||||
"version": "==5.3.1"
|
||||
},
|
||||
"coveralls": {
|
||||
"hashes": [
|
||||
@@ -961,19 +1024,19 @@
|
||||
},
|
||||
"factory-boy": {
|
||||
"hashes": [
|
||||
"sha256:d8626622550c8ba31392f9e19fdbcef9f139cf1ad643c5923f20490a7b3e2e3d",
|
||||
"sha256:ded73e49135c24bd4d3f45bf1eb168f8d290090f5cf4566b8df3698317dc9c08"
|
||||
"sha256:1d3db4b44b8c8c54cdd8b83ae4bdb9aeb121e464400035f1f03ae0e1eade56a4",
|
||||
"sha256:401cc00ff339a022f84d64a4339503d1689e8263a4478d876e58a3295b155c5b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.1.0"
|
||||
"version": "==3.2.0"
|
||||
},
|
||||
"faker": {
|
||||
"hashes": [
|
||||
"sha256:1fcb415562ee6e2395b041e85fa6901d4708d30b84d54015226fa754ed0822c3",
|
||||
"sha256:e8beccb398ee9b8cc1a91d9295121d66512b6753b4846eb1e7370545d46b3311"
|
||||
"sha256:7b0c4bb678be21a68640007f254259c73d18f7996a3448267716423360519732",
|
||||
"sha256:7e98483fc273ec5cfe1c9efa9b99adaa2de4c6b610fbc62d3767088e4974b0ce"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==5.0.1"
|
||||
"version": "==5.3.0"
|
||||
},
|
||||
"filelock": {
|
||||
"hashes": [
|
||||
@@ -1002,19 +1065,19 @@
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:6112e21359ef8f344e7178aa5b72dc6e62b38b0d008e6d3cb212c5b84df72013",
|
||||
"sha256:b0c2d3b226157ae4517d9625decf63591461c66b3a808c2666d538946519d170"
|
||||
"sha256:5c5a2720817414a6c41f0a49993908068243ae02c1635a228126519b509c8aed",
|
||||
"sha256:bf792d480abbd5eda85794e4afb09dd538393f7d6e6ffef6e9f03d2014cf9450"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==3.1.1"
|
||||
"version": "==3.3.0"
|
||||
},
|
||||
"importlib-resources": {
|
||||
"hashes": [
|
||||
"sha256:7b51f0106c8ec564b1bef3d9c588bc694ce2b92125bbb6278f4f2f5b54ec3592",
|
||||
"sha256:a3d34a8464ce1d5d7c92b0ea4e921e696d86f2aa212e684451cb1482c8d84ed5"
|
||||
"sha256:0a948d0c8c3f9344de62997e3f73444dbba233b1eaf24352933c2d264b9e4182",
|
||||
"sha256:6b45007a479c4ec21165ae3ffbe37faf35404e2041fac6ae1da684f38530ca73"
|
||||
],
|
||||
"markers": "python_version < '3.7'",
|
||||
"version": "==3.3.0"
|
||||
"version": "==4.1.1"
|
||||
},
|
||||
"iniconfig": {
|
||||
"hashes": [
|
||||
@@ -1077,11 +1140,11 @@
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:05af3bb85d320377db281cf254ab050e1a7ebcbf5410685a9a407e18a1f81236",
|
||||
"sha256:eb41423378682dadb7166144a4926e443093863024de508ca5c9737d6bc08376"
|
||||
"sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858",
|
||||
"sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==20.7"
|
||||
"version": "==20.8"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
@@ -1093,11 +1156,11 @@
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
"sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2",
|
||||
"sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"
|
||||
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
|
||||
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.9.0"
|
||||
"version": "==1.10.0"
|
||||
},
|
||||
"pycodestyle": {
|
||||
"hashes": [
|
||||
@@ -1125,11 +1188,11 @@
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe",
|
||||
"sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e"
|
||||
"sha256:1969f797a1a0dbd8ccf0fecc80262312729afea9c17f1d70ebf85c5e76c6f7c8",
|
||||
"sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==6.1.2"
|
||||
"version": "==6.2.1"
|
||||
},
|
||||
"pytest-cov": {
|
||||
"hashes": [
|
||||
@@ -1174,11 +1237,11 @@
|
||||
},
|
||||
"pytest-xdist": {
|
||||
"hashes": [
|
||||
"sha256:7c629016b3bb006b88ac68e2b31551e7becf173c76b977768848e2bbed594d90",
|
||||
"sha256:82d938f1a24186520e2d9d3a64ef7d9ac7ecdf1a0659e095d18e596b8cbd0672"
|
||||
"sha256:1d8edbb1a45e8e1f8e44b1260583107fc23f8bc8da6d18cb331ff61d41258ecf",
|
||||
"sha256:f127e11e84ad37cc1de1088cb2990f3c354630d428af3f71282de589c5bb779b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.1.0"
|
||||
"version": "==2.2.0"
|
||||
},
|
||||
"python-dateutil": {
|
||||
"hashes": [
|
||||
@@ -1190,18 +1253,18 @@
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
"sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268",
|
||||
"sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd"
|
||||
"sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4",
|
||||
"sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5"
|
||||
],
|
||||
"version": "==2020.4"
|
||||
"version": "==2020.5"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8",
|
||||
"sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"
|
||||
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
|
||||
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==2.25.0"
|
||||
"version": "==2.25.1"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
@@ -1220,19 +1283,19 @@
|
||||
},
|
||||
"sphinx": {
|
||||
"hashes": [
|
||||
"sha256:1e8d592225447104d1172be415bc2972bd1357e3e12fdc76edf2261105db4300",
|
||||
"sha256:d4e59ad4ea55efbb3c05cde3bfc83bfc14f0c95aa95c3d75346fcce186a47960"
|
||||
"sha256:77dec5ac77ca46eee54f59cf477780f4fb23327b3339ef39c8471abb829c1285",
|
||||
"sha256:b8aa4eb5502c53d3b5ca13a07abeedacd887f7770c198952fd5b9530d973e767"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.3.1"
|
||||
"version": "==3.4.2"
|
||||
},
|
||||
"sphinx-rtd-theme": {
|
||||
"hashes": [
|
||||
"sha256:22c795ba2832a169ca301cd0a083f7a434e09c538c70beb42782c073651b707d",
|
||||
"sha256:373413d0f82425aaa28fb288009bf0d0964711d347763af2f1b65cafcb028c82"
|
||||
"sha256:eda689eda0c7301a80cf122dad28b1861e5605cbf455558f3775e1e8200e83a5",
|
||||
"sha256:fa6bebd5ab9a73da8e102509a86f3fcc36dec04a0b52ea80e5a033b2aba00113"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.5.0"
|
||||
"version": "==0.5.1"
|
||||
},
|
||||
"sphinxcontrib-applehelp": {
|
||||
"hashes": [
|
||||
|
39
README.md
39
README.md
@@ -1,11 +1,12 @@
|
||||
[](https://travis-ci.org/jonaswinkler/paperless-ng)
|
||||
[](https://travis-ci.com/jonaswinkler/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)
|
||||
[](https://coveralls.io/github/jonaswinkler/paperless-ng?branch=master)
|
||||
|
||||
# Paperless-ng
|
||||
|
||||
[Paperless](https://github.com/the-paperless-project/paperless) is an application by Daniel Quinn and others that indexes your scanned documents and allows you to easily search for documents and store metadata alongside your documents.
|
||||
[Paperless](https://github.com/the-paperless-project/paperless) is an application by Daniel Quinn and contributors that indexes your scanned documents and allows you to easily search for documents and store metadata alongside your documents.
|
||||
|
||||
Paperless-ng is a fork of the original project, adding a new interface and many other changes under the hood. For a detailed list of changes, have a look at the changelog in the documentation.
|
||||
|
||||
@@ -35,15 +36,17 @@ Here's what you get:
|
||||
* Includes a dashboard that shows basic statistics and has document upload.
|
||||
* Filtering by tags, correspondents, types, and more.
|
||||
* Customizable views can be saved and displayed on the dashboard.
|
||||
* Full text search with auto completion, scored results and query highlighting allows you to quickly find what you need.
|
||||
* Full text search helps you find what you need.
|
||||
* Auto completion suggests relevant words from your documents.
|
||||
* Results are sorted by relevance to your search query.
|
||||
* Highlighting shows you which parts of the document matched the query.
|
||||
* Searching for similar documents ("More like this")
|
||||
* Email processing: Paperless adds documents from your email accounts.
|
||||
* Configure multiple accounts and filters for each account.
|
||||
* When adding documents from mails, paperless can move these mails to a new folder, mark them as read, flag them or delete them.
|
||||
* Machine learning powered document matching.
|
||||
* Paperless learns from your documents and will be able to automatically assign tags, correspondents and types to documents once you've stored a few documents in paperless.
|
||||
* A task processor that processes documents in parallel and also tells you when something goes wrong. On modern multi core systems, consumption is blazing fast.
|
||||
* Code cleanup in many, MANY areas. Some of the code from OG paperless was just overly complicated.
|
||||
* More tests, more stability.
|
||||
|
||||
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).
|
||||
|
||||
@@ -52,27 +55,38 @@ For a complete list of changes from paperless, check out the [changelog](https:/
|
||||
# Roadmap for 1.0
|
||||
|
||||
- Make the front end nice (except mobile).
|
||||
- Test coverage at 90%.
|
||||
- Fix whatever bugs I and you find.
|
||||
- Start using CI to build the app.
|
||||
- Simplify updates.
|
||||
- Make the documentation nice.
|
||||
|
||||
## Roadmap for versions beyond 1.0
|
||||
|
||||
These are things that I want to add to paperless eventually. They are sorted by priority.
|
||||
|
||||
- **Bulk editing**. Add/remove metadata from multiple documents at once.
|
||||
- **More search.** The search backend is incredibly versatile and customizable. Searching is the most important feature of this project and thus, I want to implement things like:
|
||||
- Group and limit search results by correspondent, show “more from this” links in the results.
|
||||
- Ability to search for “Similar documents” in the search results
|
||||
- **Nested tags**. Organize tags in a hierarchical structure. This will combine the benefits of folders and tags in one coherent system.
|
||||
- **An interactive consumer** that shows its progress for documents it processes on the web page.
|
||||
- With live updates ans websockets. This already works on a dev branch, but requires a lot of new dependencies, which I'm not particular happy about.
|
||||
- With live updates and websockets. This already works on a dev branch, but requires a lot of new dependencies, which I'm not particularly happy about.
|
||||
- Notifications when a document was added with buttons to open the new document right away.
|
||||
- **Arbitrary tag colors**. Allow the selection of any color with a color picker.
|
||||
|
||||
Apart from that, paperless is pretty much feature complete.
|
||||
|
||||
## On the chopping block.
|
||||
|
||||
- **GnuPG encrypion.** [Here's a note about encryption in paperless](https://paperless-ng.readthedocs.io/en/latest/administration.html#managing-encryption). The gist of it is that I don't see which attacks this implementation protects against. It gives a false sense of security to users who don't care about how it works.
|
||||
|
||||
## Wont-do list.
|
||||
|
||||
These features will probably never make it into paperless, since paperless is meant to be an easy to use set-and-forget solution.
|
||||
|
||||
- **Document versions.** I might consider adding the ability to update a document with a newer version, but that's about it. The kind of documents that get added to paperless usually don't change at all.
|
||||
- **Workflows.** I don't see a use case for these, yet.
|
||||
- **Folders.** Tags are superior in just about every way.
|
||||
- **Apps / extension support.** Again, paperless is meant to be simple.
|
||||
|
||||
# Getting started
|
||||
|
||||
The recommended way to deploy paperless is docker-compose. Don't clone the repository, grab the latest release to get started instead. The dockerfiles archive contains just the docker files which will pull the image from docker hub. The source archive contains everything you need to build the docker image yourself (i.e. if you want to run on Raspberry Pi).
|
||||
@@ -89,6 +103,12 @@ Read the section about [migration](https://paperless-ng.readthedocs.io/en/latest
|
||||
|
||||
The documentation for Paperless-ng is available on [ReadTheDocs](https://paperless-ng.readthedocs.io/).
|
||||
|
||||
# Translation
|
||||
|
||||
Paperless is currently available in English, German, Dutch and French. Translation is coordinated at transifex: https://www.transifex.com/paperless/paperless-ng
|
||||
|
||||
If you want to see paperless in your own language, request that language at transifex and you can start translating after I approve the language.
|
||||
|
||||
# Suggestions? Questions? Something not working?
|
||||
|
||||
Please open an issue and start a discussion about it!
|
||||
@@ -109,7 +129,6 @@ Paperless has been around a while now, and people are starting to build stuff on
|
||||
These projects also exist, but their status and compatibility with paperless-ng is unknown.
|
||||
|
||||
* [Paperless Desktop](https://github.com/thomasbrueggemann/paperless-desktop): A desktop UI for your Paperless installation. Runs on Mac, Linux, and Windows.
|
||||
* [ansible-role-paperless](https://github.com/ovv/ansible-role-paperless): An easy way to get Paperless running via Ansible.
|
||||
* [paperless-cli](https://github.com/stgarf/paperless-cli): A golang command line binary to interact with a Paperless instance.
|
||||
|
||||
# Important Note
|
||||
|
@@ -1,4 +1,4 @@
|
||||
bind = '127.0.0.1:8000'
|
||||
bind = '0.0.0.0:8000'
|
||||
backlog = 2048
|
||||
workers = 3
|
||||
worker_class = 'sync'
|
||||
|
@@ -15,7 +15,7 @@ services:
|
||||
POSTGRES_PASSWORD: paperless
|
||||
|
||||
webserver:
|
||||
image: jonaswinkler/paperless-ng:0.9.7
|
||||
image: jonaswinkler/paperless-ng:0.9.12
|
||||
restart: always
|
||||
depends_on:
|
||||
- db
|
||||
|
@@ -5,7 +5,7 @@ services:
|
||||
restart: always
|
||||
|
||||
webserver:
|
||||
image: jonaswinkler/paperless-ng:0.9.7
|
||||
image: jonaswinkler/paperless-ng:0.9.12
|
||||
restart: always
|
||||
depends_on:
|
||||
- broker
|
||||
|
43
docker/hub/docker-compose.tika.yml
Normal file
43
docker/hub/docker-compose.tika.yml
Normal file
@@ -0,0 +1,43 @@
|
||||
version: "3.4"
|
||||
services:
|
||||
broker:
|
||||
image: redis:6.0
|
||||
restart: always
|
||||
|
||||
webserver:
|
||||
image: jonaswinkler/paperless-ng:0.9.12
|
||||
restart: always
|
||||
depends_on:
|
||||
- broker
|
||||
ports:
|
||||
- 8000: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
|
||||
env_file: docker-compose.env
|
||||
environment:
|
||||
PAPERLESS_REDIS: redis://broker:6379
|
||||
PAPERLESS_TIKA_ENABLED: 1
|
||||
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||
|
||||
gotenberg:
|
||||
image: thecodingmachine/gotenberg
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DISABLE_GOOGLE_CHROME: 1
|
||||
|
||||
tika:
|
||||
image: apache/tika
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
data:
|
||||
media:
|
@@ -9,6 +9,9 @@ RUN apt-get update \
|
||||
&& apt-get -y --no-install-recommends install \
|
||||
build-essential \
|
||||
curl \
|
||||
file \
|
||||
fonts-liberation \
|
||||
gettext \
|
||||
ghostscript \
|
||||
gnupg \
|
||||
icc-profiles-free \
|
||||
@@ -20,6 +23,7 @@ RUN apt-get update \
|
||||
libpq-dev \
|
||||
libqpdf-dev \
|
||||
libxml2 \
|
||||
libxslt1-dev \
|
||||
optipng \
|
||||
pngquant \
|
||||
qpdf \
|
||||
@@ -60,8 +64,11 @@ WORKDIR /usr/src/paperless/src/
|
||||
|
||||
RUN sudo -HEu paperless python3 manage.py collectstatic --clear --no-input
|
||||
|
||||
RUN sudo -HEu 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"]
|
||||
EXPOSE 8000
|
||||
CMD ["/usr/local/bin/supervisord", "-c", "/etc/supervisord.conf"]
|
||||
|
||||
LABEL maintainer="Jonas Winkler <dev@jpwinkler.de>"
|
||||
|
43
docker/local/docker-compose.tika.yml
Normal file
43
docker/local/docker-compose.tika.yml
Normal file
@@ -0,0 +1,43 @@
|
||||
version: "3.4"
|
||||
services:
|
||||
broker:
|
||||
image: redis:6.0
|
||||
restart: always
|
||||
|
||||
webserver:
|
||||
build: .
|
||||
restart: always
|
||||
depends_on:
|
||||
- broker
|
||||
ports:
|
||||
- 8000: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
|
||||
env_file: docker-compose.env
|
||||
environment:
|
||||
PAPERLESS_REDIS: redis://broker:6379
|
||||
PAPERLESS_TIKA_ENABLED: 1
|
||||
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||
|
||||
gotenberg:
|
||||
image: thecodingmachine/gotenberg
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DISABLE_GOOGLE_CHROME: 1
|
||||
|
||||
tika:
|
||||
image: apache/tika
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
data:
|
||||
media:
|
@@ -8,7 +8,7 @@ loglevel=info ; log level; default info; others: debug,warn,trace
|
||||
user=root
|
||||
|
||||
[program:gunicorn]
|
||||
command=gunicorn -c /usr/src/paperless/gunicorn.conf.py -b 0.0.0.0:8000 paperless.wsgi
|
||||
command=gunicorn -c /usr/src/paperless/gunicorn.conf.py paperless.wsgi
|
||||
user=paperless
|
||||
|
||||
stdout_logfile=/dev/stdout
|
||||
|
@@ -148,7 +148,13 @@ After grabbing the new release and unpacking the contents, do the following:
|
||||
|
||||
$ cd src
|
||||
$ pipenv run python3 manage.py migrate
|
||||
|
||||
5. Update translation files.
|
||||
|
||||
.. code:: shell-session
|
||||
|
||||
$ cd src
|
||||
$ pipenv run python3 manage.py compilemessages
|
||||
|
||||
Management utilities
|
||||
####################
|
||||
|
@@ -5,85 +5,6 @@ Advanced topics
|
||||
Paperless offers a couple features that automate certain tasks and make your life
|
||||
easier.
|
||||
|
||||
Guesswork
|
||||
#########
|
||||
|
||||
|
||||
Any document you put into the consumption directory will be consumed, but if
|
||||
you name the file right, it'll automatically set some values in the database
|
||||
for you. This is is the logic the consumer follows:
|
||||
|
||||
1. Try to find the correspondent, title, and tags in the file name following
|
||||
the pattern: ``Date - Correspondent - Title - tag,tag,tag.pdf``. Note that
|
||||
the format of the date is **rigidly defined** as ``YYYYMMDDHHMMSSZ`` or
|
||||
``YYYYMMDDZ``. The ``Z`` refers "Zulu time" AKA "UTC".
|
||||
The tags are optional, so the format ``Date - Correspondent - Title.pdf``
|
||||
works as well.
|
||||
2. If that doesn't work, we skip the date and try this pattern:
|
||||
``Correspondent - Title - tag,tag,tag.pdf``.
|
||||
3. If that doesn't work, we try to find the correspondent and title in the file
|
||||
name following the pattern: ``Correspondent - Title.pdf``.
|
||||
4. If that doesn't work, just assume that the name of the file is the title.
|
||||
|
||||
So given the above, the following examples would work as you'd expect:
|
||||
|
||||
* ``20150314000700Z - Some Company Name - Invoice 2016-01-01 - money,invoices.pdf``
|
||||
* ``20150314Z - Some Company Name - Invoice 2016-01-01 - money,invoices.pdf``
|
||||
* ``Some Company Name - Invoice 2016-01-01 - money,invoices.pdf``
|
||||
* ``Another Company - Letter of Reference.jpg``
|
||||
* ``Dad's Recipe for Pancakes.png``
|
||||
|
||||
These however wouldn't work:
|
||||
|
||||
* ``2015-03-14 00:07:00 UTC - Some Company Name, Invoice 2016-01-01, money, invoices.pdf``
|
||||
* ``2015-03-14 - Some Company Name, Invoice 2016-01-01, money, invoices.pdf``
|
||||
* ``Some Company Name, Invoice 2016-01-01, money, invoices.pdf``
|
||||
* ``Another Company- Letter of Reference.jpg``
|
||||
|
||||
Do I have to be so strict about naming?
|
||||
=======================================
|
||||
|
||||
Rather than using the strict document naming rules, one can also set the option
|
||||
``PAPERLESS_FILENAME_DATE_ORDER`` in ``paperless.conf`` to any date order
|
||||
that is accepted by dateparser_. Doing so will cause ``paperless`` to default
|
||||
to any date format that is found in the title, instead of a date pulled from
|
||||
the document's text, without requiring the strict formatting of the document
|
||||
filename as described above.
|
||||
|
||||
.. _dateparser: https://github.com/scrapinghub/dateparser/blob/v0.7.0/docs/usage.rst#settings
|
||||
|
||||
.. _advanced-transforming_filenames:
|
||||
|
||||
Transforming filenames for parsing
|
||||
==================================
|
||||
|
||||
Some devices can't produce filenames that can be parsed by the default
|
||||
parser. By configuring the option ``PAPERLESS_FILENAME_PARSE_TRANSFORMS`` in
|
||||
``paperless.conf`` one can add transformations that are applied to the filename
|
||||
before it's parsed.
|
||||
|
||||
The option contains a list of dictionaries of regular expressions (key:
|
||||
``pattern``) and replacements (key: ``repl``) in JSON format, which are
|
||||
applied in order by passing them to ``re.subn``. Transformation stops
|
||||
after the first match, so at most one transformation is applied. The general
|
||||
syntax is
|
||||
|
||||
.. code:: python
|
||||
|
||||
[{"pattern":"pattern1", "repl":"repl1"}, {"pattern":"pattern2", "repl":"repl2"}, ..., {"pattern":"patternN", "repl":"replN"}]
|
||||
|
||||
The example below is for a Brother ADS-2400N, a scanner that allows
|
||||
different names to different hardware buttons (useful for handling
|
||||
multiple entities in one instance), but insists on adding ``_<count>``
|
||||
to the filename.
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Brother profile configuration, support "Name_Date_Count" (the default
|
||||
# setting) and "Name_Count" (use "Name" as tag and "Count" as title).
|
||||
PAPERLESS_FILENAME_PARSE_TRANSFORMS=[{"pattern":"^([a-z]+)_(\\d{8})_(\\d{6})_([0-9]+)\\.", "repl":"\\2\\3Z - \\4 - \\1."}, {"pattern":"^([a-z]+)_([0-9]+)\\.", "repl":" - \\2 - \\1."}]
|
||||
|
||||
|
||||
.. _advanced-matching:
|
||||
|
||||
Matching tags, correspondents and document types
|
||||
|
15
docs/api.rst
15
docs/api.rst
@@ -221,21 +221,16 @@ Each fragment contains a list of strings, and some of them are marked as a highl
|
||||
|
||||
[
|
||||
[
|
||||
{"text": "This is a sample text with a "},
|
||||
{"text": "highlighted", "term": 0},
|
||||
{"text": " word."}
|
||||
{"text": "This is a sample text with a ", "highlight": false},
|
||||
{"text": "highlighted", "highlight": true},
|
||||
{"text": " word.", "highlight": false}
|
||||
],
|
||||
[
|
||||
{"text": "Another", "term": 1},
|
||||
{"text": " fragment with a highlight."}
|
||||
{"text": "Another", "highlight": true},
|
||||
{"text": " fragment with a highlight.", "highlight": false}
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
|
||||
When ``term`` is present within a string, the word within ``text`` should be highlighted.
|
||||
The term index groups multiple matches together and words with the same index
|
||||
should get identical highlighting.
|
||||
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. ...
|
||||
|
@@ -5,6 +5,129 @@
|
||||
Changelog
|
||||
*********
|
||||
|
||||
paperless-ng 0.9.12
|
||||
###################
|
||||
|
||||
* Paperless localization
|
||||
|
||||
* Thanks to the combined efforts of many users, Paperless is now available in English, Dutch, French and German.
|
||||
|
||||
* Thanks to `Jo Vandeginste`_, Paperless has optional support for Office documents such as .docx, .doc, .odt and more.
|
||||
|
||||
* See the :ref:`configuration<configuration-tika>` on how to enable this feature. This feature requires two additional services
|
||||
(one for parsing Office documents and metadata extraction and another for converting Office documents to PDF), and is therefore
|
||||
not enabled on default installations.
|
||||
* As with all other documents, paperless converts Office documents to PDF and stores both the original as well as the archived PDF.
|
||||
|
||||
* Dark mode
|
||||
|
||||
* Thanks to `Michael Shamoon`_, paperless now has a dark mode. Configuration is available in the settings.
|
||||
|
||||
* Other changes and additions
|
||||
|
||||
* The PDF viewer now uses a local copy of some dependencies instead of fetching them from the internet. Thanks to `slorenz`_.
|
||||
* Revamped search bar styling thanks to `Michael Shamoon`_.
|
||||
* Sorting in the document list by clicking on table headers.
|
||||
* A button was added to the document detail page that assigns a new ASN to a document.
|
||||
* Form field validation: When providing invalid input in a form (such as a duplicate ASN or no name), paperless now has visual
|
||||
indicators and clearer error messages about what's wrong.
|
||||
* Paperless disables buttons with network actions (such as save and delete) when a network action is active. This indicates that
|
||||
something is happening and prevents double clicking.
|
||||
* When using "Save & next", the title field is focussed automatically to better support keyboard editing.
|
||||
* E-Mail: Added filter rule parameters to allow inline attachments (watch out for mails with inlined images!) and attachment filename filters
|
||||
with wildcards.
|
||||
* Support for remote user authentication thanks to `Michael Shamoon`_. This is useful for hiding Paperless behind single sign on applications
|
||||
such as `authelia <https://www.authelia.com/>`_.
|
||||
* "Clear filters" has been renamed to "Reset filters" and now correctly restores the default filters on saved views. Thanks to `Michael Shamoon`_
|
||||
|
||||
* Fixes
|
||||
|
||||
* Paperless was unable to save views when "Not assigned" was chosen in one of the filter dropdowns.
|
||||
* Clearer error messages when pre and post consumption scripts do not exist.
|
||||
* The post consumption script is executed later in the consumption process. Before the change, an ID was passed to the script referring to
|
||||
a document that did not yet exist in the database.
|
||||
|
||||
paperless-ng 0.9.11
|
||||
###################
|
||||
|
||||
* Fixed an issue with the docker image not starting at all due to a configuration change of the web server.
|
||||
|
||||
|
||||
paperless-ng 0.9.10
|
||||
###################
|
||||
|
||||
* Bulk editing
|
||||
|
||||
* Thanks to `Michael Shamoon`_, we've got a new interface for the bulk editor.
|
||||
* There are some configuration options in the settings to alter the behavior.
|
||||
|
||||
* Other changes and additions
|
||||
|
||||
* Thanks to `zjean`_, paperless now publishes a webmanifest, which is useful for adding the application to home screens on mobile devices.
|
||||
* The Paperless-ng logo now navigates to the dashboard.
|
||||
* Filter for documents that don't have any correspondents, types or tags assigned.
|
||||
* Tags, types and correspondents are now sorted case insensitive.
|
||||
* Lots of preparation work for localization support.
|
||||
|
||||
* Fixes
|
||||
|
||||
* Added missing dependencies for Raspberry Pi builds.
|
||||
* Fixed an issue with plain text file consumption: Thumbnail generation failed due to missing fonts.
|
||||
* An issue with the search index reporting missing documents after bulk deletes was fixed.
|
||||
* Issue with the tag selector not clearing input correctly.
|
||||
* The consumer used to stop working when encountering an incomplete classifier model file.
|
||||
|
||||
.. note::
|
||||
|
||||
The bulk delete operations did not update the search index. Therefore, documents that you deleted remained in the index and
|
||||
caused the search to return messages about missing documents when searching. Further bulk operations will properly update
|
||||
the index.
|
||||
|
||||
However, this change is not retroactive: If you used the delete method of the bulk editor, you need to reindex your search index
|
||||
by :ref:`running the management command document_index with the argument reindex <administration-index>`.
|
||||
|
||||
paperless-ng 0.9.9
|
||||
##################
|
||||
|
||||
Christmas release!
|
||||
|
||||
* Bulk editing
|
||||
|
||||
* Paperless now supports bulk editing.
|
||||
* The following operations are available: Add and remove correspondents, tags, document types from selected documents, as well as mass-deleting documents.
|
||||
* We've got a more fancy UI in the works that makes these features more accessible, but that's not quite ready yet.
|
||||
|
||||
* Searching
|
||||
|
||||
* Paperless now supports searching for similar documents ("More like this") both from the document detail page as well as from individual search results.
|
||||
* A search score indicates how well a document matches the search query, or how similar a document is to a given reference document.
|
||||
|
||||
* Other additions and changes
|
||||
|
||||
* Clarification in the UI that the fields "Match" and "Is insensitive" are not relevant for the Auto matching algorithm.
|
||||
* New select interface for tags, types and correspondents allows filtering. This also improves tag selection. Thanks again to `Michael Shamoon`_!
|
||||
* Page navigation controls for the document viewer, thanks to `Michael Shamoon`_.
|
||||
* Layout changes to the small cards document list.
|
||||
* The dashboard now displays the username (or full name if specified in the admin) on the dashboard.
|
||||
|
||||
* Fixes
|
||||
|
||||
* An error that caused the document importer to crash was fixed.
|
||||
* An issue with changes not being possible when ``PAPERLESS_COOKIE_PREFIX`` is used was fixed.
|
||||
* The date selection filters now allow manual entry of dates.
|
||||
|
||||
* Feature Removal
|
||||
|
||||
* Most of the guesswork features have been removed. Paperless no longer tries to extract correspondents and tags from file names.
|
||||
|
||||
paperless-ng 0.9.8
|
||||
##################
|
||||
|
||||
This release addresses two severe issues with the previous release.
|
||||
|
||||
* The delete buttons for document types, correspondents and tags were not working.
|
||||
* The document section in the admin was causing internal server errors (500).
|
||||
|
||||
|
||||
paperless-ng 0.9.7
|
||||
##################
|
||||
@@ -884,6 +1007,9 @@ bulk of the work on this big change.
|
||||
|
||||
* Initial release
|
||||
|
||||
.. _slorenz: https://github.com/sisao
|
||||
.. _Jo Vandeginste: https://github.com/jovandeginste
|
||||
.. _zjean: https://github.com/zjean
|
||||
.. _rYR79435: https://github.com/rYR79435
|
||||
.. _Michael Shamoon: https://github.com/shamoon
|
||||
.. _jayme-github: http://github.com/jayme-github
|
||||
|
@@ -162,6 +162,12 @@ PAPERLESS_COOKIE_PREFIX=<str>
|
||||
|
||||
Defaults to ``""``, which does not alter the cookie names.
|
||||
|
||||
PAPERLESS_ENABLE_HTTP_REMOTE_USER=<bool>
|
||||
Allows authentication via HTTP_REMOTE_USER which is used by some SSO
|
||||
applications.
|
||||
|
||||
Defaults to `false` which disables this feature.
|
||||
|
||||
.. _configuration-ocr:
|
||||
|
||||
OCR settings
|
||||
@@ -210,20 +216,20 @@ PAPERLESS_OCR_MODE=<mode>
|
||||
into images and puts the OCRed text on top. This works for all documents,
|
||||
however, the resulting document may be significantly larger and text
|
||||
won't appear as sharp when zoomed in.
|
||||
|
||||
|
||||
The default is ``skip``, which only performs OCR when necessary and always
|
||||
creates archived documents.
|
||||
|
||||
PAPERLESS_OCR_OUTPUT_TYPE=<type>
|
||||
Specify the the type of PDF documents that paperless should produce.
|
||||
|
||||
|
||||
* ``pdf``: Modify the PDF document as little as possible.
|
||||
* ``pdfa``: Convert PDF documents into PDF/A-2b documents, which is a
|
||||
subset of the entire PDF specification and meant for storing
|
||||
documents long term.
|
||||
* ``pdfa-1``, ``pdfa-2``, ``pdfa-3`` to specify the exact version of
|
||||
PDF/A you wish to use.
|
||||
|
||||
|
||||
If not specified, ``pdfa`` is used. Remember that paperless also keeps
|
||||
the original input file as well as the archived version.
|
||||
|
||||
@@ -275,9 +281,69 @@ PAPERLESS_OCR_USER_ARG=<json>
|
||||
|
||||
.. code:: json
|
||||
|
||||
{"deskew": true, "optimize": 3, "unpaper_args": "--pre-rotate 90"}
|
||||
|
||||
|
||||
{"deskew": true, "optimize": 3, "unpaper_args": "--pre-rotate 90"}
|
||||
|
||||
.. _configuration-tika:
|
||||
|
||||
Tika settings
|
||||
#############
|
||||
|
||||
Paperless can make use of `Tika <https://tika.apache.org/>`_ and
|
||||
`Gotenberg <https://thecodingmachine.github.io/gotenberg/>`_ for parsing and
|
||||
converting "Office" documents (such as ".doc", ".xlsx" and ".odt"). If you
|
||||
wish to use this, you must provide a Tika server and a Gotenberg server,
|
||||
configure their endpoints, and enable the feature.
|
||||
|
||||
PAPERLESS_TIKA_ENABLED=<bool>
|
||||
Enable (or disable) the Tika parser.
|
||||
|
||||
Defaults to false.
|
||||
|
||||
PAPERLESS_TIKA_ENDPOINT=<url>
|
||||
Set the endpoint URL were Paperless can reach your Tika server.
|
||||
|
||||
Defaults to "http://localhost:9998".
|
||||
|
||||
PAPERLESS_TIKA_GOTENBERG_ENDPOINT=<url>
|
||||
Set the endpoint URL were Paperless can reach your Gotenberg server.
|
||||
|
||||
Defaults to "http://localhost:3000".
|
||||
|
||||
If you run paperless on docker, you can add those services to the docker-compose
|
||||
file (see the provided ``docker-compose.tika.yml`` file for reference). The changes
|
||||
requires are as follows:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
services:
|
||||
# ...
|
||||
|
||||
webserver:
|
||||
# ...
|
||||
|
||||
environment:
|
||||
# ...
|
||||
|
||||
PAPERLESS_TIKA_ENABLED: 1
|
||||
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||
|
||||
# ...
|
||||
|
||||
gotenberg:
|
||||
image: thecodingmachine/gotenberg
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DISABLE_GOOGLE_CHROME: 1
|
||||
|
||||
tika:
|
||||
image: apache/tika
|
||||
restart: unless-stopped
|
||||
|
||||
Add the configuration variables to the environment of the webserver (alternatively
|
||||
put the configuration in the ``docker-compose.env`` file) and add the additional
|
||||
services below the webserver service. Watch out for indentation.
|
||||
|
||||
Software tweaks
|
||||
###############
|
||||
|
||||
@@ -319,11 +385,14 @@ PAPERLESS_TIME_ZONE=<timezone>
|
||||
Defaults to UTC.
|
||||
|
||||
|
||||
.. _configuration-polling:
|
||||
|
||||
PAPERLESS_CONSUMER_POLLING=<num>
|
||||
If paperless won't find documents added to your consume folder, it might
|
||||
not be able to automatically detect filesystem changes. In that case,
|
||||
specify a polling interval in seconds here, which will then cause paperless
|
||||
to periodically check your consumption directory for changes.
|
||||
to periodically check your consumption directory for changes. This will also
|
||||
disable listening for file system changes with ``inotify``.
|
||||
|
||||
Defaults to 0, which disables polling and uses filesystem notifications.
|
||||
|
||||
@@ -400,11 +469,28 @@ PAPERLESS_FILENAME_DATE_ORDER=<format>
|
||||
|
||||
Defaults to none, which disables this feature.
|
||||
|
||||
PAPERLESS_FILENAME_PARSE_TRANSFORMS
|
||||
Transforms filenames before they are processed by paperless. See
|
||||
:ref:`advanced-transforming_filenames` for details.
|
||||
PAPERLESS_THUMBNAIL_FONT_NAME=<filename>
|
||||
Paperless creates thumbnails for plain text files by rendering the content
|
||||
of the file on an image and uses a predefined font for that. This
|
||||
font can be changed here.
|
||||
|
||||
Note that this won't have any effect on already generated thumbnails.
|
||||
|
||||
Defaults to ``/usr/share/fonts/liberation/LiberationSerif-Regular.ttf``.
|
||||
|
||||
PAPERLESS_IGNORE_DATES=<string>
|
||||
Paperless parses a documents creation date from filename and file content.
|
||||
You may specify a comma separated list of dates that should be ignored during
|
||||
this process. This is useful for special dates (like date of birth) that appear
|
||||
in documents regularly but are very unlikely to be the documents creation date.
|
||||
|
||||
You may specify dates in a multitude of formats supported by dateparser (see
|
||||
https://dateparser.readthedocs.io/en/latest/#popular-formats) but as the dates
|
||||
need to be comma separated, the options are limited.
|
||||
Example: "2020-12-02,22.04.1999"
|
||||
|
||||
Defaults to an empty string to not ignore any dates.
|
||||
|
||||
Defaults to none, which disables this feature.
|
||||
|
||||
Binaries
|
||||
########
|
||||
|
@@ -25,6 +25,8 @@ that works right for you based on recommendations from other Paperless users.
|
||||
+---------+----------------+-----+-----+-----+----------------+
|
||||
| Fujitsu | `ix500`_ | yes | | yes | `eonist`_ |
|
||||
+---------+----------------+-----+-----+-----+----------------+
|
||||
| Epson | `WF-7710DWF`_ | yes | | yes | `Skylinar`_ |
|
||||
+---------+----------------+-----+-----+-----+----------------+
|
||||
| Fujitsu | `S1300i`_ | yes | | yes | `jonaswinkler`_|
|
||||
+---------+----------------+-----+-----+-----+----------------+
|
||||
|
||||
@@ -32,7 +34,8 @@ that works right for you based on recommendations from other Paperless users.
|
||||
.. _MFC-J6930DW: https://www.brother.ca/en/p/MFCJ6930DW
|
||||
.. _MFC-J5910DW: https://www.brother.co.uk/printers/inkjet-printers/mfcj5910dw
|
||||
.. _MFC-9142CDN: https://www.brother.co.uk/printers/laser-printers/mfc9140cdn
|
||||
.. _ix500: https://www.fujitsu.com/global/products/computing/peripheral/scanners/scansnap/ix500/
|
||||
.. _ix500: http://www.fujitsu.com/us/products/computing/peripheral/scanners/scansnap/ix500/
|
||||
.. _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
|
||||
@@ -40,4 +43,5 @@ that works right for you based on recommendations from other Paperless users.
|
||||
.. _bmsleight: https://github.com/bmsleight
|
||||
.. _eonist: https://github.com/eonist
|
||||
.. _REOLDEV: https://github.com/REOLDEV
|
||||
.. _Skylinar: https://github.com/Skylinar
|
||||
.. _jonaswinkler: https://github.com/jonaswinkler
|
||||
|
@@ -120,6 +120,8 @@ The `bare metal route`_ is more complicated to setup but makes it easier
|
||||
should you want to contribute some code back. You need to configure and
|
||||
run the above mentioned components yourself.
|
||||
|
||||
.. _setup-docker_route:
|
||||
|
||||
Docker Route
|
||||
============
|
||||
|
||||
@@ -177,6 +179,14 @@ Docker Route
|
||||
|
||||
You can use any settings from the file ``paperless.conf`` in this file.
|
||||
Have a look at :ref:`configuration` to see whats available.
|
||||
|
||||
.. caution::
|
||||
|
||||
Certain file systems such as NFS network shares don't support file system
|
||||
notifications with ``inotify``. When storing the consumption directory
|
||||
on such a file system, paperless will be unable to pick up new files
|
||||
with the default configuration. You will need to use ``PAPERLESS_CONSUMER_POLLING``,
|
||||
which will disable inotify. See :ref:`here <configuration-polling>`.
|
||||
|
||||
4. Run ``docker-compose up -d``. This will create and start the necessary
|
||||
containers. This will also build the image of paperless if you grabbed the
|
||||
@@ -219,8 +229,9 @@ writing. Windows is not and will never be supported.
|
||||
* ``python3-pip``, optionally ``pipenv`` for package installation
|
||||
* ``python3-dev``
|
||||
|
||||
* ``fonts-liberation`` for generating thumbnails for plain text files
|
||||
* ``imagemagick`` >= 6 for PDF conversion
|
||||
* ``optipng`` for optimising thumbnails
|
||||
* ``optipng`` for optimizing thumbnails
|
||||
* ``gnupg`` for handling encrypted documents
|
||||
* ``libpoppler-cpp-dev`` for PDF to text conversion
|
||||
* ``libmagic-dev`` for mime type detection
|
||||
@@ -240,8 +251,7 @@ writing. Windows is not and will never be supported.
|
||||
* ``tesseract-ocr`` language packs (``tesseract-ocr-eng``, ``tesseract-ocr-deu``, etc)
|
||||
|
||||
You will also need ``build-essential``, ``python3-setuptools`` and ``python3-wheel``
|
||||
for installing some of the python dependencies. You can remove that
|
||||
again after installation.
|
||||
for installing some of the python dependencies.
|
||||
|
||||
2. Install ``redis`` >= 5.0 and configure it to start automatically.
|
||||
|
||||
@@ -290,6 +300,9 @@ writing. Windows is not and will never be supported.
|
||||
|
||||
# This creates the database schema.
|
||||
python3 manage.py migrate
|
||||
|
||||
# This creates the translation files for paperless.
|
||||
python3 manage.py compilemessages
|
||||
|
||||
# This creates your first paperless user
|
||||
python3 manage.py createsuperuser
|
||||
@@ -460,6 +473,15 @@ management commands as below.
|
||||
load data from an old database schema in SQLite into a newer database
|
||||
schema in PostgreSQL, you will run into trouble.
|
||||
|
||||
.. warning::
|
||||
|
||||
On some database fields, PostgreSQL enforces predefined limits on maximum
|
||||
length, whereas SQLite does not. The fields in question are the title of documents
|
||||
(128 characters), names of document types, tags and correspondents (128 characters),
|
||||
and filenames (1024 characters). If you have data in these fields that surpasses these
|
||||
limits, migration to PostgreSQL is not possible and will fail with an error.
|
||||
|
||||
|
||||
1. Stop paperless, if it is running.
|
||||
2. Tell paperless to use PostgreSQL:
|
||||
|
||||
|
@@ -34,12 +34,15 @@ directory at startup, but won't find any other files added later, check out
|
||||
the configuration file and enable filesystem polling with the setting
|
||||
``PAPERLESS_CONSUMER_POLLING``.
|
||||
|
||||
This will disable listening to filesystem changes with inotify and paperless will
|
||||
manually check the consumption directory for changes instead.
|
||||
|
||||
Operation not permitted
|
||||
#######################
|
||||
|
||||
You might see errors such as:
|
||||
|
||||
.. code::
|
||||
.. code:: shell-session
|
||||
|
||||
chown: changing ownership of '../export': Operation not permitted
|
||||
|
||||
@@ -49,3 +52,29 @@ to these folders. This happens when pointing these directories to NFS shares,
|
||||
for example.
|
||||
|
||||
Ensure that `chown` is possible on these directories.
|
||||
|
||||
Classifier error: No training data available
|
||||
############################################
|
||||
|
||||
This indicates that the Auto matching algorithm found no documents to learn from.
|
||||
This may have two reasons:
|
||||
|
||||
* You don't use the Auto matching algorithm: The error can be safely ignored in this case.
|
||||
* You are using the Auto matching algorithm: The classifier explicitly excludes documents
|
||||
with Inbox tags. Verify that there are documents in your archive without inbox tags.
|
||||
The algorithm will only learn from documents not in your inbox.
|
||||
|
||||
Permission denied errors in the consumption directory
|
||||
#####################################################
|
||||
|
||||
You might encounter errors such as:
|
||||
|
||||
.. code:: shell-session
|
||||
|
||||
The following error occured while consuming document.pdf: [Errno 13] Permission denied: '/usr/src/paperless/src/../consume/document.pdf'
|
||||
|
||||
This happens when paperless does not have permission to delete files inside the consumption directory.
|
||||
Ensure that ``USERMAP_UID`` and ``USERMAP_GID`` are set to the user id and group id you use on the host operating system, if these are
|
||||
different from ``1000``. See :ref:`setup-docker_route`.
|
||||
|
||||
Also ensure that you are able to read and write to the consumption directory on the host.
|
||||
|
@@ -31,6 +31,7 @@
|
||||
#PAPERLESS_STATIC_URL=/static/
|
||||
#PAPERLESS_AUTO_LOGIN_USERNAME=
|
||||
#PAPERLESS_COOKIE_PREFIX=
|
||||
#PAPERLESS_ENABLE_HTTP_REMOTE_USER=false
|
||||
|
||||
# OCR settings
|
||||
|
||||
@@ -39,7 +40,7 @@
|
||||
#PAPERLESS_OCR_OUTPUT_TYPE=pdfa
|
||||
#PAPERLESS_OCR_PAGES=1
|
||||
#PAPERLESS_OCR_IMAGE_DPI=300
|
||||
#PAPERLESS_OCR_USER_ARG={}
|
||||
#PAPERLESS_OCR_USER_ARGS={}
|
||||
#PAPERLESS_CONVERT_MEMORY_LIMIT=0
|
||||
#PAPERLESS_CONVERT_TMPDIR=/var/tmp/paperless
|
||||
|
||||
@@ -50,10 +51,20 @@
|
||||
#PAPERLESS_TIME_ZONE=UTC
|
||||
#PAPERLESS_CONSUMER_POLLING=10
|
||||
#PAPERLESS_CONSUMER_DELETE_DUPLICATES=false
|
||||
#PAPERLESS_CONSUMER_RECURSIVE=false
|
||||
#PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=false
|
||||
#PAPERLESS_OPTIMIZE_THUMBNAILS=true
|
||||
#PAPERLESS_POST_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh
|
||||
#PAPERLESS_FILENAME_DATE_ORDER=YMD
|
||||
#PAPERLESS_FILENAME_PARSE_TRANSFORMS=[]
|
||||
#PAPERLESS_THUMBNAIL_FONT_NAME=
|
||||
#PAPERLESS_IGNORE_DATES=
|
||||
|
||||
# Tika settings
|
||||
|
||||
#PAPERLESS_TIKA_ENABLED=false
|
||||
#PAPERLESS_TIKA_ENDPOINT=http://localhost:9998
|
||||
#PAPERLESS_TIKA_GOTENBERG_ENDPOINT=http://localhost:3000
|
||||
|
||||
# Binaries
|
||||
|
||||
|
@@ -57,8 +57,8 @@ pipenv lock --keep-outdated -r > "$PAPERLESS_DIST_APP/requirements.txt"
|
||||
# test if the application works.
|
||||
|
||||
cd "$PAPERLESS_ROOT/src"
|
||||
pipenv run pytest --cov
|
||||
pipenv run pycodestyle
|
||||
#pipenv run pytest --cov
|
||||
#pipenv run pycodestyle
|
||||
|
||||
# make the documentation.
|
||||
|
||||
@@ -81,7 +81,7 @@ cp "$PAPERLESS_ROOT/paperless.conf.example" "$PAPERLESS_DIST_APP/paperless.conf"
|
||||
|
||||
# copy python source, templates and static files.
|
||||
cd "$PAPERLESS_ROOT"
|
||||
find src -wholename '*/templates/*' -o -wholename '*/static/*' -o -name '*.py' | cpio -pdm "$PAPERLESS_DIST_APP"
|
||||
find src -name '*.po' -o -wholename '*/templates/*' -o -wholename '*/static/*' -o -name '*.py' | cpio -pdm "$PAPERLESS_DIST_APP"
|
||||
|
||||
# build the front end.
|
||||
|
||||
|
@@ -1,2 +1,4 @@
|
||||
docker run -p 5432:5432 -v paperless_pgdata:/var/lib/postgresql/data -d postgres:13
|
||||
docker run -d -p 6379:6379 redis:latest
|
||||
docker run -p 3000:3000 -d thecodingmachine/gotenberg
|
||||
docker run -p 9998:9998 -d apache/tika
|
||||
|
@@ -13,6 +13,14 @@
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"i18n": {
|
||||
"sourceLocale": "en-US",
|
||||
"locales": {
|
||||
"de": "src/locale/messages.de.xlf",
|
||||
"nl-NL": "src/locale/messages.nl_NL.xlf",
|
||||
"fr": "src/locale/messages.fr.xlf"
|
||||
}
|
||||
},
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
@@ -23,15 +31,24 @@
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"localize": true,
|
||||
"aot": true,
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
"src/assets",
|
||||
"src/manifest.webmanifest", {
|
||||
"glob": "pdf.worker.min.js",
|
||||
"input": "node_modules/pdfjs-dist/build/",
|
||||
"output": "/assets/js/"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
"scripts": [],
|
||||
"allowedCommonJsDependencies": [
|
||||
"ng2-pdf-viewer"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
@@ -61,13 +78,16 @@
|
||||
"maximumError": "10kb"
|
||||
}
|
||||
]
|
||||
},
|
||||
"en-US": {
|
||||
"localize": ["en-US"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "paperless-ui:build"
|
||||
"browserTarget": "paperless-ui:build:en-US"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
@@ -90,7 +110,8 @@
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
"src/assets",
|
||||
"src/manifest.webmanifest"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
@@ -127,4 +148,4 @@
|
||||
}
|
||||
},
|
||||
"defaultProject": "paperless-ui"
|
||||
}
|
||||
}
|
||||
|
1671
src-ui/messages.xlf
Normal file
1671
src-ui/messages.xlf
Normal file
File diff suppressed because it is too large
Load Diff
25
src-ui/package-lock.json
generated
25
src-ui/package-lock.json
generated
@@ -331,6 +331,12 @@
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
|
||||
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
|
||||
"dev": true
|
||||
},
|
||||
"uuid": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz",
|
||||
@@ -2056,6 +2062,14 @@
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@ng-select/ng-select": {
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-5.0.9.tgz",
|
||||
"integrity": "sha512-YZeSAiS8/Nx/eHZJPmOOYL8YmcvSq+dr1P8WIrsKmRA7mueorBpPc5xlUj+nLQbpLtsiQvdWDQspf/ykOvD/lA==",
|
||||
"requires": {
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@ngtools/webpack": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-10.2.0.tgz",
|
||||
@@ -2170,6 +2184,14 @@
|
||||
"pacote": "9.5.12",
|
||||
"semver": "7.3.2",
|
||||
"semver-intersect": "1.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
|
||||
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@types/glob": {
|
||||
@@ -6476,8 +6498,7 @@
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
|
||||
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
|
||||
"resolved": "",
|
||||
"dev": true
|
||||
},
|
||||
"inquirer": {
|
||||
|
@@ -21,6 +21,7 @@
|
||||
"@angular/platform-browser-dynamic": "~10.1.5",
|
||||
"@angular/router": "~10.1.5",
|
||||
"@ng-bootstrap/ng-bootstrap": "^8.0.0",
|
||||
"@ng-select/ng-select": "^5.0.9",
|
||||
"bootstrap": "^4.5.0",
|
||||
"ng-bootstrap": "^1.6.3",
|
||||
"ng2-pdf-viewer": "^6.3.2",
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { SettingsService } from './services/settings.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -6,9 +7,11 @@ import { Component } from '@angular/core';
|
||||
styleUrls: ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent {
|
||||
|
||||
constructor () {
|
||||
|
||||
constructor (private settings: SettingsService) {
|
||||
let anyWindow = (window as any)
|
||||
anyWindow.pdfWorkerSrc = '/assets/js/pdf.worker.min.js';
|
||||
this.settings.updateDarkModeSettings()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@@ -26,12 +26,13 @@ import { ResultHighlightComponent } from './components/search/result-highlight/r
|
||||
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';
|
||||
import { FilterEditorComponent } from './components/filter-editor/filter-editor.component';
|
||||
import { FilterDropdownComponent } from './components/filter-editor/filter-dropdown/filter-dropdown.component';
|
||||
import { FilterDropdownButtonComponent } from './components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component';
|
||||
import { FilterDropdownDateComponent } from './components/filter-editor/filter-dropdown-date/filter-dropdown-date.component';
|
||||
import { FilterEditorComponent } from './components/document-list/filter-editor/filter-editor.component';
|
||||
import { FilterableDropdownComponent } from './components/common/filterable-dropdown/filterable-dropdown.component';
|
||||
import { ToggleableDropdownButtonComponent } from './components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component';
|
||||
import { DateDropdownComponent } from './components/common/date-dropdown/date-dropdown.component';
|
||||
import { DocumentCardLargeComponent } from './components/document-list/document-card-large/document-card-large.component';
|
||||
import { DocumentCardSmallComponent } from './components/document-list/document-card-small/document-card-small.component';
|
||||
import { BulkEditorComponent } from './components/document-list/bulk-editor/bulk-editor.component';
|
||||
import { NgxFileDropModule } from 'ngx-file-drop';
|
||||
import { TextComponent } from './components/common/input/text/text.component';
|
||||
import { SelectComponent } from './components/common/input/select/select.component';
|
||||
@@ -54,6 +55,9 @@ import { FileSizePipe } from './pipes/file-size.pipe';
|
||||
import { FilterPipe } from './pipes/filter.pipe';
|
||||
import { DocumentTitlePipe } from './pipes/document-title.pipe';
|
||||
import { MetadataCollapseComponent } from './components/document-detail/metadata-collapse/metadata-collapse.component';
|
||||
import { SelectDialogComponent } from './components/common/select-dialog/select-dialog.component';
|
||||
import { NgSelectModule } from '@ng-select/ng-select';
|
||||
import { NumberComponent } from './components/common/input/number/number.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -78,11 +82,12 @@ import { MetadataCollapseComponent } from './components/document-detail/metadata
|
||||
AppFrameComponent,
|
||||
ToastsComponent,
|
||||
FilterEditorComponent,
|
||||
FilterDropdownComponent,
|
||||
FilterDropdownButtonComponent,
|
||||
FilterDropdownDateComponent,
|
||||
FilterableDropdownComponent,
|
||||
ToggleableDropdownButtonComponent,
|
||||
DateDropdownComponent,
|
||||
DocumentCardLargeComponent,
|
||||
DocumentCardSmallComponent,
|
||||
BulkEditorComponent,
|
||||
TextComponent,
|
||||
SelectComponent,
|
||||
CheckComponent,
|
||||
@@ -99,7 +104,9 @@ import { MetadataCollapseComponent } from './components/document-detail/metadata
|
||||
FileSizePipe,
|
||||
FilterPipe,
|
||||
DocumentTitlePipe,
|
||||
MetadataCollapseComponent
|
||||
MetadataCollapseComponent,
|
||||
SelectDialogComponent,
|
||||
NumberComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@@ -110,7 +117,8 @@ import { MetadataCollapseComponent } from './components/document-detail/metadata
|
||||
ReactiveFormsModule,
|
||||
NgxFileDropModule,
|
||||
InfiniteScrollModule,
|
||||
PdfViewerModule
|
||||
PdfViewerModule,
|
||||
NgSelectModule
|
||||
],
|
||||
providers: [
|
||||
DatePipe,
|
||||
|
@@ -1,17 +1,52 @@
|
||||
<nav class="navbar navbar-dark sticky-top bg-primary flex-md-nowrap p-0 shadow">
|
||||
<span class="navbar-brand col-md-3 col-lg-2 mr-0 px-3" href="#">
|
||||
<img src="assets/logo-dark-notext.svg" height="18px" class="mr-2">
|
||||
Paperless-ng
|
||||
</span>
|
||||
<button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-toggle="collapse"
|
||||
<button class="navbar-toggler d-md-none collapsed border-0" type="button" data-toggle="collapse"
|
||||
data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation"
|
||||
(click)="isMenuCollapsed = !isMenuCollapsed">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<form (ngSubmit)="search()" class="w-100 m-1">
|
||||
<input class="form-control form-control-dark" type="text" placeholder="Search" aria-label="Search"
|
||||
[formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (selectItem)="itemSelected($event)">
|
||||
</form>
|
||||
<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">
|
||||
<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>
|
||||
</a>
|
||||
<div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 pl-md-4 mr-sm-auto order-3 order-sm-1">
|
||||
<form (ngSubmit)="search()" class="form-inline flex-grow-1">
|
||||
<input class="form-control form-control-sm" type="text" placeholder="Search documents" aria-label="Search"
|
||||
[formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (selectItem)="itemSelected($event)" i18n-placeholder>
|
||||
<svg width="1em" height="1em">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#search"/>
|
||||
</svg>
|
||||
</form>
|
||||
</div>
|
||||
<ul ngbNav class="order-sm-3">
|
||||
<li ngbDropdown class="nav-item dropdown">
|
||||
<button class="btn text-light" id="userDropdown" ngbDropdownToggle>
|
||||
<span *ngIf="displayName" class="navbar-text small mr-2 text-light d-none d-sm-inline">
|
||||
{{displayName}}
|
||||
</span>
|
||||
<svg width="1.3em" height="1.3em">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#person-circle"/>
|
||||
</svg>
|
||||
</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>
|
||||
<div class="dropdown-divider"></div>
|
||||
</div>
|
||||
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()">
|
||||
<svg class="sidebaricon mr-2" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
|
||||
</svg><ng-container i18n>Settings</ng-container>
|
||||
</a>
|
||||
<a ngbDropdownItem class="nav-link" href="accounts/logout/">
|
||||
<svg class="sidebaricon mr-2" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#door-open"/>
|
||||
</svg><ng-container i18n>Logout</ng-container>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid">
|
||||
@@ -28,136 +63,115 @@
|
||||
<a class="nav-link" routerLink="dashboard" routerLinkActive="active" (click)="closeMenu()">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#house"/>
|
||||
</svg>
|
||||
Dashboard
|
||||
</svg> <ng-container i18n>Dashboard</ng-container>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="documents" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" (click)="closeMenu()">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#files"/>
|
||||
</svg>
|
||||
Documents
|
||||
</svg> <ng-container i18n>Documents</ng-container>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted" *ngIf='savedViewService.sidebarViews.length > 0'>
|
||||
<span>Saved views</span>
|
||||
<ng-container i18n>Saved views</ng-container>
|
||||
</h6>
|
||||
<ul class="nav flex-column mb-2">
|
||||
<li class="nav-item w-100" *ngFor="let view of savedViewService.sidebarViews">
|
||||
<a class="nav-link text-truncate" routerLink="view/{{view.id}}" routerLinkActive="active" (click)="closeMenu()">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#funnel"/>
|
||||
</svg>
|
||||
{{view.name}}
|
||||
</svg> {{view.name}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted" *ngIf='openDocuments.length > 0'>
|
||||
<span>Open documents</span>
|
||||
<ng-container i18n>Open documents</ng-container>
|
||||
</h6>
|
||||
<ul class="nav flex-column mb-2">
|
||||
<li class="nav-item w-100" *ngFor='let d of openDocuments'>
|
||||
<a class="nav-link text-truncate" routerLink="documents/{{d.id}}" routerLinkActive="active" (click)="closeMenu()">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
|
||||
</svg>
|
||||
{{d.title | documentTitle}}
|
||||
</svg> {{d.title | documentTitle}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item w-100" *ngIf="openDocuments.length > 1">
|
||||
<a class="nav-link text-truncate" [routerLink]="" (click)="closeAll()">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg>
|
||||
Close all
|
||||
</svg> <ng-container i18n>Close all</ng-container>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>Manage</span>
|
||||
<ng-container i18n>Manage</ng-container>
|
||||
</h6>
|
||||
<ul class="nav flex-column mb-2">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#person"/>
|
||||
</svg>
|
||||
Correspondents
|
||||
</svg> <ng-container i18n>Correspondents</ng-container>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#tags"/>
|
||||
</svg>
|
||||
Tags
|
||||
</svg> <ng-container i18n>Tags</ng-container>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#hash"/>
|
||||
</svg>
|
||||
Document types
|
||||
</svg> <ng-container i18n>Document types</ng-container>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#text-left"/>
|
||||
</svg>
|
||||
Logs
|
||||
</svg> <ng-container i18n>Logs</ng-container>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
|
||||
</svg>
|
||||
Settings
|
||||
</svg> <ng-container i18n>Settings</ng-container>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="admin/">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#toggles"/>
|
||||
</svg>
|
||||
Admin
|
||||
</svg> <ng-container i18n>Admin</ng-container>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>Misc</span>
|
||||
<ng-container i18n>Misc</ng-container>
|
||||
</h6>
|
||||
<ul class="nav flex-column mb-2">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" target="_blank" rel="noopener noreferrer" href="https://paperless-ng.readthedocs.io/en/latest/">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#question-circle"/>
|
||||
</svg>
|
||||
Documentation
|
||||
</svg> <ng-container i18n>Documentation</ng-container>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" target="_blank" rel="noopener noreferrer" href="https://github.com/jonaswinkler/paperless-ng">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#link"/>
|
||||
</svg>
|
||||
GitHub
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="accounts/logout/">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#door-open"/>
|
||||
</svg>
|
||||
Logout
|
||||
</svg> <ng-container i18n>GitHub</ng-container>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
@@ -1,36 +1,30 @@
|
||||
|
||||
@import "/src/theme";
|
||||
|
||||
/*
|
||||
/*
|
||||
* Sidebar
|
||||
*/
|
||||
|
||||
.sidebar {
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 100; /* Behind the navbar */
|
||||
padding: 48px 0 0; /* Height of navbar */
|
||||
padding: 50px 0 0; /* Height of navbar */
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.sidebar {
|
||||
top: 3rem;
|
||||
top: 3.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-sticky {
|
||||
position: relative;
|
||||
top: 0;
|
||||
/* height: calc(100vh - 48px); */
|
||||
height: 100%;
|
||||
padding-top: .5rem;
|
||||
padding-top: 0.5rem;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
|
||||
}
|
||||
|
||||
@supports ((position: -webkit-sticky) or (position: sticky)) {
|
||||
.sidebar-sticky {
|
||||
position: -webkit-sticky;
|
||||
@@ -53,36 +47,85 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.sidebar .nav-link:hover .sidebaricon,
|
||||
.sidebar .nav-link.active .sidebaricon {
|
||||
.sidebar .nav-link.active .sidebaricon,
|
||||
.sidebar .nav-link:hover .sidebaricon {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.sidebar-heading {
|
||||
font-size: .75rem;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Navbar
|
||||
*/
|
||||
|
||||
.navbar-brand {
|
||||
padding-top: .75rem;
|
||||
padding-bottom: .75rem;
|
||||
.navbar-brand {
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
font-size: 1rem;
|
||||
background-color: rgba(0, 0, 0, .25);
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
|
||||
}
|
||||
|
||||
.navbar .navbar-toggler {
|
||||
top: .25rem;
|
||||
right: 1rem;
|
||||
.dropdown.show .dropdown-toggle,
|
||||
.dropdown-toggle:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.navbar .form-control {
|
||||
padding: .75rem 1rem;
|
||||
border-width: 0;
|
||||
border-radius: 0;
|
||||
.dropdown-toggle::after {
|
||||
margin-left: 0.4em;
|
||||
vertical-align: 0.155em;
|
||||
}
|
||||
|
||||
.navbar .dropdown-menu {
|
||||
font-size: 0.875rem; // body size
|
||||
|
||||
a svg {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar .search-form-container {
|
||||
max-width: 550px;
|
||||
|
||||
form {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
svg {
|
||||
position: absolute;
|
||||
left: 0.6rem;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
svg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.form-control::placeholder {
|
||||
color: rgba(255, 255, 255, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.form-control {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
padding-left: 1.8rem;
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
transition: flex 0.3s ease;
|
||||
max-width: 600px;
|
||||
min-width: 300px; // 1/2 max
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background-color: #fff;
|
||||
color: #212529;
|
||||
flex-grow: 1;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -9,7 +9,8 @@ import { SavedViewService } from 'src/app/services/rest/saved-view.service';
|
||||
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';
|
||||
|
||||
@Component({
|
||||
selector: 'app-app-frame',
|
||||
templateUrl: './app-frame.component.html',
|
||||
@@ -22,8 +23,10 @@ export class AppFrameComponent implements OnInit, OnDestroy {
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private openDocumentsService: OpenDocumentsService,
|
||||
private searchService: SearchService,
|
||||
public savedViewService: SavedViewService
|
||||
public savedViewService: SavedViewService,
|
||||
private meta: Meta
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
versionString = `${environment.appTitle} ${environment.version}`
|
||||
@@ -55,7 +58,7 @@ export class AppFrameComponent implements OnInit, OnDestroy {
|
||||
term.length < 2 ? from([[]]) : this.searchService.autocomplete(term)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
itemSelected(event) {
|
||||
event.preventDefault()
|
||||
let currentSearch: string = this.searchField.value
|
||||
@@ -98,4 +101,17 @@ export class AppFrameComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
get displayName() {
|
||||
// TODO: taken from dashboard component, is this the best way to pass around username?
|
||||
let tagFullName = this.meta.getTag('name=full_name')
|
||||
let tagUsername = this.meta.getTag('name=username')
|
||||
if (tagFullName && tagFullName.content) {
|
||||
return tagFullName.content
|
||||
} else if (tagUsername && tagUsername.content) {
|
||||
return tagUsername.content
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -9,6 +9,9 @@
|
||||
<p *ngIf="message">{{message}}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-dark" (click)="cancelClicked()">Cancel</button>
|
||||
<button type="button" class="btn" [class]="btnClass" (click)="confirmClicked.emit()">{{btnCaption}}</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-dark" (click)="cancelClicked()" [disabled]="!buttonsEnabled" i18n>Cancel</button>
|
||||
<button type="button" class="btn" [class]="btnClass" (click)="confirmClicked.emit()" [disabled]="!confirmButtonEnabled || !buttonsEnabled">
|
||||
{{btnCaption}}
|
||||
<span *ngIf="!confirmButtonEnabled"> ({{seconds}})</span>
|
||||
</button>
|
||||
</div>
|
||||
|
@@ -14,7 +14,7 @@ export class ConfirmDialogComponent implements OnInit {
|
||||
public confirmClicked = new EventEmitter()
|
||||
|
||||
@Input()
|
||||
title = "Confirmation"
|
||||
title = $localize`Confirmation`
|
||||
|
||||
@Input()
|
||||
messageBold
|
||||
@@ -26,7 +26,25 @@ export class ConfirmDialogComponent implements OnInit {
|
||||
btnClass = "btn-primary"
|
||||
|
||||
@Input()
|
||||
btnCaption = "Confirm"
|
||||
btnCaption = $localize`Confirm`
|
||||
|
||||
@Input()
|
||||
buttonsEnabled = true
|
||||
|
||||
confirmButtonEnabled = true
|
||||
seconds = 0
|
||||
|
||||
delayConfirm(seconds: number) {
|
||||
this.confirmButtonEnabled = false
|
||||
this.seconds = seconds
|
||||
setTimeout(() => {
|
||||
if (this.seconds <= 1) {
|
||||
this.confirmButtonEnabled = true
|
||||
} else {
|
||||
this.delayConfirm(seconds - 1)
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
@@ -0,0 +1,44 @@
|
||||
<div class="btn-group" ngbDropdown role="group">
|
||||
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="dateBefore || dateAfter ? 'btn-primary' : 'btn-outline-primary'">
|
||||
{{title}}
|
||||
</button>
|
||||
<div class="dropdown-menu date-dropdown shadow pt-0" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
|
||||
<div class="list-group list-group-flush">
|
||||
<button *ngFor="let qf of quickFilters" class="list-group-item small list-goup list-group-item-action d-flex p-2 pl-3" role="menuitem" (click)="setDateQuickFilter(qf.id)">
|
||||
{{qf.name}}
|
||||
</button>
|
||||
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
|
||||
|
||||
<div class="mb-2 d-flex flex-row w-100 justify-content-between small">
|
||||
<div i18n>After</div>
|
||||
<a *ngIf="dateAfter" class="btn btn-link p-0 m-0" (click)="clearAfter()">
|
||||
<svg width="0.8em" height="0.8em" viewBox="0 0 16 16" class="bi bi-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
|
||||
</svg>
|
||||
<small i18n>Clear</small>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="date" class="form-control" id="date_after" [(ngModel)]="dateAfter" (change)="onChangeDebounce()">
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
|
||||
|
||||
<div class="mb-2 d-flex flex-row w-100 justify-content-between small">
|
||||
<div i18n>Before</div>
|
||||
<a *ngIf="dateBefore" class="btn btn-link p-0 m-0" (click)="clearBefore()">
|
||||
<svg width="0.8em" height="0.8em" viewBox="0 0 16 16" class="bi bi-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
|
||||
</svg>
|
||||
<small i18n>Clear</small>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="date" class="form-control" id="date_before" [(ngModel)]="dateBefore" (change)="onChangeDebounce()">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -1,4 +1,4 @@
|
||||
.date-filter {
|
||||
.date-dropdown {
|
||||
min-width: 250px;
|
||||
|
||||
.btn-link {
|
@@ -1,20 +1,20 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FilterDropodownComponent } from './filter-dropdown.component';
|
||||
import { DateDropdownComponent } from './date-dropdown.component';
|
||||
|
||||
describe('FilterDropodownComponent', () => {
|
||||
let component: FilterDropodownComponent;
|
||||
let fixture: ComponentFixture<FilterDropodownComponent>;
|
||||
describe('DateDropdownComponent', () => {
|
||||
let component: DateDropdownComponent;
|
||||
let fixture: ComponentFixture<DateDropdownComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ FilterDropodownComponent ]
|
||||
declarations: [ DateDropdownComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FilterDropodownComponent);
|
||||
fixture = TestBed.createComponent(DateDropdownComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
@@ -0,0 +1,111 @@
|
||||
import { formatDate } from '@angular/common';
|
||||
import { Component, EventEmitter, Input, Output, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
||||
|
||||
export interface DateSelection {
|
||||
before?: string
|
||||
after?: string
|
||||
}
|
||||
|
||||
const LAST_7_DAYS = 0
|
||||
const LAST_MONTH = 1
|
||||
const LAST_3_MONTHS = 2
|
||||
const LAST_YEAR = 3
|
||||
|
||||
@Component({
|
||||
selector: 'app-date-dropdown',
|
||||
templateUrl: './date-dropdown.component.html',
|
||||
styleUrls: ['./date-dropdown.component.scss']
|
||||
})
|
||||
export class DateDropdownComponent implements OnInit, OnDestroy {
|
||||
|
||||
quickFilters = [
|
||||
{id: LAST_7_DAYS, name: $localize`Last 7 days`},
|
||||
{id: LAST_MONTH, name: $localize`Last month`},
|
||||
{id: LAST_3_MONTHS, name: $localize`Last 3 months`},
|
||||
{id: LAST_YEAR, name: $localize`Last year`}
|
||||
]
|
||||
|
||||
@Input()
|
||||
dateBefore: string
|
||||
|
||||
@Output()
|
||||
dateBeforeChange = new EventEmitter<string>()
|
||||
|
||||
@Input()
|
||||
dateAfter: string
|
||||
|
||||
@Output()
|
||||
dateAfterChange = new EventEmitter<string>()
|
||||
|
||||
@Input()
|
||||
title: string
|
||||
|
||||
@Output()
|
||||
datesSet = new EventEmitter<DateSelection>()
|
||||
|
||||
private datesSetDebounce$ = new Subject()
|
||||
|
||||
private sub: Subscription
|
||||
|
||||
ngOnInit() {
|
||||
this.sub = this.datesSetDebounce$.pipe(
|
||||
debounceTime(400)
|
||||
).subscribe(() => {
|
||||
this.onChange()
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.sub) {
|
||||
this.sub.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
setDateQuickFilter(qf: number) {
|
||||
this.dateBefore = null
|
||||
let date = new Date()
|
||||
switch (qf) {
|
||||
case LAST_7_DAYS:
|
||||
date.setDate(date.getDate() - 7)
|
||||
break;
|
||||
|
||||
case LAST_MONTH:
|
||||
date.setMonth(date.getMonth() - 1)
|
||||
break;
|
||||
|
||||
case LAST_3_MONTHS:
|
||||
date.setMonth(date.getMonth() - 3)
|
||||
break
|
||||
|
||||
case LAST_YEAR:
|
||||
date.setFullYear(date.getFullYear() - 1)
|
||||
break
|
||||
|
||||
}
|
||||
this.dateAfter = formatDate(date, 'yyyy-MM-dd', "en-us", "UTC")
|
||||
this.onChange()
|
||||
}
|
||||
|
||||
onChange() {
|
||||
this.dateAfterChange.emit(this.dateAfter)
|
||||
this.dateBeforeChange.emit(this.dateBefore)
|
||||
this.datesSet.emit({after: this.dateAfter, before: this.dateBefore})
|
||||
}
|
||||
|
||||
onChangeDebounce() {
|
||||
this.datesSetDebounce$.next({after: this.dateAfter, before: this.dateBefore})
|
||||
}
|
||||
|
||||
clearBefore() {
|
||||
this.dateBefore = null
|
||||
this.onChange()
|
||||
}
|
||||
|
||||
clearAfter() {
|
||||
this.dateAfter = null
|
||||
this.onChange()
|
||||
}
|
||||
|
||||
}
|
@@ -2,10 +2,11 @@ import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { Observable } from 'rxjs';
|
||||
import { MATCHING_ALGORITHMS } from 'src/app/data/matching-model';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model';
|
||||
import { ObjectWithId } from 'src/app/data/object-with-id';
|
||||
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service';
|
||||
import { Toast, ToastService } from 'src/app/services/toast.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
@Directive()
|
||||
export abstract class EditDialogComponent<T extends ObjectWithId> implements OnInit {
|
||||
@@ -13,8 +14,7 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI
|
||||
constructor(
|
||||
private service: AbstractPaperlessService<T>,
|
||||
private activeModal: NgbActiveModal,
|
||||
private toastService: ToastService,
|
||||
private entityName: string) { }
|
||||
private toastService: ToastService) { }
|
||||
|
||||
@Input()
|
||||
dialogMode: string = 'create'
|
||||
@@ -25,6 +25,10 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI
|
||||
@Output()
|
||||
success = new EventEmitter()
|
||||
|
||||
networkActive = false
|
||||
|
||||
error = null
|
||||
|
||||
abstract getForm(): FormGroup
|
||||
|
||||
objectForm: FormGroup = this.getForm()
|
||||
@@ -35,12 +39,24 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI
|
||||
}
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
return $localize`Create new item`
|
||||
}
|
||||
|
||||
getEditTitle() {
|
||||
return $localize`Edit item`
|
||||
}
|
||||
|
||||
getSaveErrorMessage(error: string) {
|
||||
return $localize`Could not save element: ${error}`
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
switch (this.dialogMode) {
|
||||
case 'create':
|
||||
return "Create new " + this.entityName
|
||||
return this.getCreateTitle()
|
||||
case 'edit':
|
||||
return "Edit " + this.entityName
|
||||
return this.getEditTitle()
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -50,6 +66,10 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI
|
||||
return MATCHING_ALGORITHMS
|
||||
}
|
||||
|
||||
get patternRequired(): boolean {
|
||||
return this.objectForm?.value.matching_algorithm !== MATCH_AUTO
|
||||
}
|
||||
|
||||
save() {
|
||||
var newObject = Object.assign(Object.assign({}, this.object), this.objectForm.value)
|
||||
var serverResponse: Observable<T>
|
||||
@@ -62,11 +82,13 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI
|
||||
default:
|
||||
break;
|
||||
}
|
||||
this.networkActive = true
|
||||
serverResponse.subscribe(result => {
|
||||
this.activeModal.close()
|
||||
this.success.emit(result)
|
||||
}, error => {
|
||||
this.toastService.showToast(Toast.makeError(`Could not save ${this.entityName}: ${error.error.name}`))
|
||||
this.error = error.error
|
||||
this.networkActive = false
|
||||
})
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,35 @@
|
||||
<div class="btn-group" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown">
|
||||
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'">
|
||||
<div class="d-none d-md-inline">{{title}}</div>
|
||||
<div class="d-inline-block d-md-none">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
|
||||
</svg>
|
||||
</div>
|
||||
<ng-container *ngIf="!editing && selectionModel.selectionSize() > 0">
|
||||
<div class="badge bg-secondary text-light rounded-pill badge-corner">
|
||||
{{selectionModel.selectionSize()}}
|
||||
</div>
|
||||
</ng-container>
|
||||
</button>
|
||||
<div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
|
||||
<div class="list-group list-group-flush">
|
||||
<div class="list-group-item">
|
||||
<div class="input-group input-group-sm">
|
||||
<input class="form-control" type="text" [(ngModel)]="filterText" [placeholder]="filterPlaceholder" (keyup.enter)="listFilterEnter()" #listFilterTextInput>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="selectionModel.items" class="items">
|
||||
<ng-container *ngFor="let item of (editing ? selectionModel.itemsSorted : selectionModel.items) | filter: filterText">
|
||||
<app-toggleable-dropdown-button *ngIf="allowSelectNone || item.id" [item]="item" [state]="selectionModel.get(item.id)" (toggle)="selectionModel.toggle(item.id)"></app-toggleable-dropdown-button>
|
||||
</ng-container>
|
||||
</div>
|
||||
<button *ngIf="editing" class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!selectionModel.isDirty()">
|
||||
<small class="ml-1" [ngClass]="{'font-weight-bold': selectionModel.isDirty()}" i18n>Apply</small>
|
||||
<svg width="1.5em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-right" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FilterableDropodownComponent } from './filterable-dropdown.component';
|
||||
|
||||
describe('FilterableDropodownComponent', () => {
|
||||
let component: FilterableDropodownComponent;
|
||||
let fixture: ComponentFixture<FilterableDropodownComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ FilterableDropodownComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FilterableDropodownComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -0,0 +1,267 @@
|
||||
import { Component, EventEmitter, Input, Output, ElementRef, ViewChild } from '@angular/core';
|
||||
import { FilterPipe } from 'src/app/pipes/filter.pipe';
|
||||
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ToggleableItemState } from './toggleable-dropdown-button/toggleable-dropdown-button.component';
|
||||
import { MatchingModel } from 'src/app/data/matching-model';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
export interface ChangedItems {
|
||||
itemsToAdd: MatchingModel[],
|
||||
itemsToRemove: MatchingModel[]
|
||||
}
|
||||
|
||||
export class FilterableDropdownSelectionModel {
|
||||
|
||||
changed = new Subject<FilterableDropdownSelectionModel>()
|
||||
|
||||
multiple = false
|
||||
|
||||
items: MatchingModel[] = []
|
||||
|
||||
get itemsSorted(): MatchingModel[] {
|
||||
return this.items.sort((a,b) => {
|
||||
if (this.getNonTemporary(a.id) == ToggleableItemState.NotSelected && this.getNonTemporary(b.id) != ToggleableItemState.NotSelected) {
|
||||
return 1
|
||||
} else if (this.getNonTemporary(a.id) != ToggleableItemState.NotSelected && this.getNonTemporary(b.id) == ToggleableItemState.NotSelected) {
|
||||
return -1
|
||||
} else {
|
||||
return a.name.localeCompare(b.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private selectionStates = new Map<number, ToggleableItemState>()
|
||||
|
||||
private temporarySelectionStates = new Map<number, ToggleableItemState>()
|
||||
|
||||
getSelectedItems() {
|
||||
return this.items.filter(i => this.temporarySelectionStates.get(i.id) == ToggleableItemState.Selected)
|
||||
}
|
||||
|
||||
set(id: number, state: ToggleableItemState, fireEvent = true) {
|
||||
if (state == ToggleableItemState.NotSelected) {
|
||||
this.temporarySelectionStates.delete(id)
|
||||
} else {
|
||||
this.temporarySelectionStates.set(id, state)
|
||||
}
|
||||
if (fireEvent) {
|
||||
this.changed.next(this)
|
||||
}
|
||||
}
|
||||
|
||||
toggle(id: number, fireEvent = true) {
|
||||
let state = this.temporarySelectionStates.get(id)
|
||||
if (state == null || state != ToggleableItemState.Selected) {
|
||||
this.temporarySelectionStates.set(id, ToggleableItemState.Selected)
|
||||
} else if (state == ToggleableItemState.Selected) {
|
||||
this.temporarySelectionStates.delete(id)
|
||||
}
|
||||
|
||||
if (!this.multiple) {
|
||||
for (let key of this.temporarySelectionStates.keys()) {
|
||||
if (key != id) {
|
||||
this.temporarySelectionStates.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
for (let key of this.temporarySelectionStates.keys()) {
|
||||
if (key) {
|
||||
this.temporarySelectionStates.delete(key)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.temporarySelectionStates.delete(null)
|
||||
}
|
||||
|
||||
if (fireEvent) {
|
||||
this.changed.next(this)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private getNonTemporary(id: number) {
|
||||
return this.selectionStates.get(id) || ToggleableItemState.NotSelected
|
||||
}
|
||||
|
||||
get(id: number) {
|
||||
return this.temporarySelectionStates.get(id) || ToggleableItemState.NotSelected
|
||||
}
|
||||
|
||||
selectionSize() {
|
||||
return this.getSelectedItems().length
|
||||
}
|
||||
|
||||
clear(fireEvent = true) {
|
||||
this.temporarySelectionStates.clear()
|
||||
if (fireEvent) {
|
||||
this.changed.next(this)
|
||||
}
|
||||
}
|
||||
|
||||
isDirty() {
|
||||
if (!Array.from(this.temporarySelectionStates.keys()).every(id => this.temporarySelectionStates.get(id) == this.selectionStates.get(id))) {
|
||||
return true
|
||||
} else if (!Array.from(this.selectionStates.keys()).every(id => this.selectionStates.get(id) == this.temporarySelectionStates.get(id))) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
isNoneSelected() {
|
||||
return this.selectionSize() == 1 && this.get(null) == ToggleableItemState.Selected
|
||||
}
|
||||
|
||||
init(map) {
|
||||
this.temporarySelectionStates = map
|
||||
this.apply()
|
||||
}
|
||||
|
||||
apply() {
|
||||
this.selectionStates.clear()
|
||||
this.temporarySelectionStates.forEach((value, key) => {
|
||||
this.selectionStates.set(key, value)
|
||||
})
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.temporarySelectionStates.clear()
|
||||
this.selectionStates.forEach((value, key) => {
|
||||
this.temporarySelectionStates.set(key, value)
|
||||
})
|
||||
}
|
||||
|
||||
diff(): ChangedItems {
|
||||
return {
|
||||
itemsToAdd: this.items.filter(item => this.temporarySelectionStates.get(item.id) == ToggleableItemState.Selected && this.selectionStates.get(item.id) != ToggleableItemState.Selected),
|
||||
itemsToRemove: this.items.filter(item => !this.temporarySelectionStates.has(item.id) && this.selectionStates.has(item.id)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-filterable-dropdown',
|
||||
templateUrl: './filterable-dropdown.component.html',
|
||||
styleUrls: ['./filterable-dropdown.component.scss']
|
||||
})
|
||||
export class FilterableDropdownComponent {
|
||||
|
||||
@ViewChild('listFilterTextInput') listFilterTextInput: ElementRef
|
||||
@ViewChild('dropdown') dropdown: NgbDropdown
|
||||
|
||||
filterText: string
|
||||
|
||||
@Input()
|
||||
set items(items: MatchingModel[]) {
|
||||
if (items) {
|
||||
this._selectionModel.items = Array.from(items)
|
||||
this._selectionModel.items.unshift({
|
||||
name: $localize`:Filter drop down element to filter for documents with no correspondent/type/tag assigned:Not assigned`,
|
||||
id: null
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
get items(): MatchingModel[] {
|
||||
return this._selectionModel.items
|
||||
}
|
||||
|
||||
_selectionModel = new FilterableDropdownSelectionModel()
|
||||
|
||||
@Input()
|
||||
set selectionModel(model: FilterableDropdownSelectionModel) {
|
||||
if (this.selectionModel) {
|
||||
this.selectionModel.changed.complete()
|
||||
model.items = this.selectionModel.items
|
||||
model.multiple = this.selectionModel.multiple
|
||||
}
|
||||
model.changed.subscribe(updatedModel => {
|
||||
this.selectionModelChange.next(updatedModel)
|
||||
})
|
||||
this._selectionModel = model
|
||||
}
|
||||
|
||||
get selectionModel(): FilterableDropdownSelectionModel {
|
||||
return this._selectionModel
|
||||
}
|
||||
|
||||
@Output()
|
||||
selectionModelChange = new EventEmitter<FilterableDropdownSelectionModel>()
|
||||
|
||||
@Input()
|
||||
set multiple(value: boolean) {
|
||||
this.selectionModel.multiple = value
|
||||
}
|
||||
|
||||
get multiple() {
|
||||
return this.selectionModel.multiple
|
||||
}
|
||||
|
||||
@Input()
|
||||
title: string
|
||||
|
||||
@Input()
|
||||
filterPlaceholder: string = ""
|
||||
|
||||
@Input()
|
||||
icon: string
|
||||
|
||||
@Input()
|
||||
allowSelectNone: boolean = false
|
||||
|
||||
@Input()
|
||||
editing = false
|
||||
|
||||
@Input()
|
||||
applyOnClose = false
|
||||
|
||||
@Output()
|
||||
apply = new EventEmitter<ChangedItems>()
|
||||
|
||||
@Output()
|
||||
open = new EventEmitter()
|
||||
|
||||
constructor(private filterPipe: FilterPipe) {
|
||||
this.selectionModel = new FilterableDropdownSelectionModel()
|
||||
}
|
||||
|
||||
applyClicked() {
|
||||
if (this.selectionModel.isDirty()) {
|
||||
this.dropdown.close()
|
||||
if (!this.applyOnClose) {
|
||||
this.apply.emit(this.selectionModel.diff())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dropdownOpenChange(open: boolean): void {
|
||||
if (open) {
|
||||
setTimeout(() => {
|
||||
this.listFilterTextInput.nativeElement.focus();
|
||||
}, 0)
|
||||
if (this.editing) {
|
||||
this.selectionModel.reset()
|
||||
}
|
||||
this.open.next()
|
||||
} else {
|
||||
this.filterText = ''
|
||||
if (this.applyOnClose && this.selectionModel.isDirty()) {
|
||||
this.apply.emit(this.selectionModel.diff())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
listFilterEnter(): void {
|
||||
let filtered = this.filterPipe.transform(this.items, this.filterText)
|
||||
if (filtered.length == 1) {
|
||||
this.selectionModel.toggle(filtered[0].id)
|
||||
if (this.editing) {
|
||||
this.applyClicked()
|
||||
} else {
|
||||
this.dropdown.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-left-0 border-right-0 border-bottom" role="menuitem" (click)="toggleItem()">
|
||||
<div class="selected-icon mr-1">
|
||||
<ng-container *ngIf="isChecked()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16">
|
||||
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
|
||||
</svg>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="isPartiallyChecked()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-dash" viewBox="0 0 16 16">
|
||||
<path d="M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8z"/>
|
||||
</svg>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
<div class="mr-1">
|
||||
<app-tag *ngIf="isTag; else displayName" [tag]="item" [clickable]="true" linkTitle="Filter by tag"></app-tag>
|
||||
<ng-template #displayName><small>{{item.name}}</small></ng-template>
|
||||
</div>
|
||||
<div class="badge badge-light rounded-pill ml-auto mr-1">{{item.document_count}}</div>
|
||||
</button>
|
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ToggleableDropdownButtonComponent } from './toggleable-dropdown-button.component';
|
||||
|
||||
describe('ToggleableDropdownButtonComponent', () => {
|
||||
let component: ToggleableDropdownButtonComponent;
|
||||
let fixture: ComponentFixture<ToggleableDropdownButtonComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ ToggleableDropdownButtonComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ToggleableDropdownButtonComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -0,0 +1,51 @@
|
||||
import { Component, EventEmitter, Input, Output, OnInit } from '@angular/core';
|
||||
import { MatchingModel } from 'src/app/data/matching-model';
|
||||
|
||||
export interface ToggleableItem {
|
||||
item: MatchingModel,
|
||||
state: ToggleableItemState,
|
||||
count: number
|
||||
}
|
||||
|
||||
export enum ToggleableItemState {
|
||||
NotSelected = 0,
|
||||
Selected = 1,
|
||||
PartiallySelected = 2
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-toggleable-dropdown-button',
|
||||
templateUrl: './toggleable-dropdown-button.component.html',
|
||||
styleUrls: ['./toggleable-dropdown-button.component.scss']
|
||||
})
|
||||
export class ToggleableDropdownButtonComponent {
|
||||
|
||||
@Input()
|
||||
item: MatchingModel
|
||||
|
||||
@Input()
|
||||
state: ToggleableItemState
|
||||
|
||||
@Input()
|
||||
count: number
|
||||
|
||||
@Output()
|
||||
toggle = new EventEmitter()
|
||||
|
||||
get isTag(): boolean {
|
||||
return 'is_inbox_tag' in this.item
|
||||
}
|
||||
|
||||
toggleItem(): void {
|
||||
this.toggle.emit()
|
||||
}
|
||||
|
||||
isChecked() {
|
||||
return this.state == ToggleableItemState.Selected
|
||||
}
|
||||
|
||||
isPartiallyChecked() {
|
||||
return this.state == ToggleableItemState.PartiallySelected
|
||||
}
|
||||
|
||||
}
|
@@ -1,10 +1,13 @@
|
||||
import { Directive, Input, OnInit } from '@angular/core';
|
||||
import { Directive, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
|
||||
import { ControlValueAccessor } from '@angular/forms';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@Directive()
|
||||
export class AbstractInputComponent<T> implements OnInit, ControlValueAccessor {
|
||||
|
||||
@ViewChild("inputField")
|
||||
inputField: ElementRef
|
||||
|
||||
constructor() { }
|
||||
|
||||
onChange = (newValue: T) => {};
|
||||
@@ -24,12 +27,21 @@ export class AbstractInputComponent<T> implements OnInit, ControlValueAccessor {
|
||||
this.disabled = isDisabled;
|
||||
}
|
||||
|
||||
focus() {
|
||||
if (this.inputField && this.inputField.nativeElement) {
|
||||
this.inputField.nativeElement.focus()
|
||||
}
|
||||
}
|
||||
|
||||
@Input()
|
||||
title: string
|
||||
|
||||
@Input()
|
||||
disabled = false;
|
||||
|
||||
@Input()
|
||||
error: string
|
||||
|
||||
value: T
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@@ -0,0 +1,14 @@
|
||||
<div class="form-group">
|
||||
<label [for]="inputId">{{title}}</label>
|
||||
<div class="input-group" [class.is-invalid]="error">
|
||||
<input type="number" class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [class.is-invalid]="error">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary" type="button" id="button-addon1" (click)="nextAsn()" [disabled]="value">+1</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="invalid-feedback">
|
||||
{{error}}
|
||||
</div>
|
||||
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
|
||||
|
||||
</div>
|
@@ -1,20 +1,20 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FilterDropdownDateComponent } from './filter-dropdown-date.component';
|
||||
import { NumberComponent } from './number.component';
|
||||
|
||||
describe('FilterDropdownDateComponent', () => {
|
||||
let component: FilterDropdownDateComponent;
|
||||
let fixture: ComponentFixture<FilterDropdownDateComponent>;
|
||||
describe('NumberComponent', () => {
|
||||
let component: NumberComponent;
|
||||
let fixture: ComponentFixture<NumberComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ FilterDropdownDateComponent ]
|
||||
declarations: [ NumberComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FilterDropdownDateComponent);
|
||||
fixture = TestBed.createComponent(NumberComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
@@ -0,0 +1,39 @@
|
||||
import { Component, forwardRef } from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { FILTER_ASN_ISNULL } from 'src/app/data/filter-rule-type';
|
||||
import { DocumentService } from 'src/app/services/rest/document.service';
|
||||
import { AbstractInputComponent } from '../abstract-input';
|
||||
|
||||
@Component({
|
||||
providers: [{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => NumberComponent),
|
||||
multi: true
|
||||
}],
|
||||
selector: 'app-input-number',
|
||||
templateUrl: './number.component.html',
|
||||
styleUrls: ['./number.component.scss']
|
||||
})
|
||||
export class NumberComponent extends AbstractInputComponent<number> {
|
||||
|
||||
constructor(private documentService: DocumentService) {
|
||||
super()
|
||||
}
|
||||
|
||||
nextAsn() {
|
||||
if (this.value) {
|
||||
return
|
||||
}
|
||||
this.documentService.listFiltered(1, 1, "archive_serial_number", true, [{rule_type: FILTER_ASN_ISNULL, value: "false"}]).subscribe(
|
||||
results => {
|
||||
if (results.count > 0) {
|
||||
this.value = results.results[0].archive_serial_number + 1
|
||||
} else {
|
||||
this.value = 1
|
||||
}
|
||||
this.onChange(this.value)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@@ -1,11 +1,16 @@
|
||||
<div class="form-group">
|
||||
<div class="form-group paperless-input-select">
|
||||
<label [for]="inputId">{{title}}</label>
|
||||
<div [class.input-group]="showPlusButton()">
|
||||
<select class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()"
|
||||
[disabled]="disabled" [style.color]="textColor" [style.background]="backgroundColor">
|
||||
<option *ngIf="allowNull" [ngValue]="null" class="form-control">---</option>
|
||||
<option *ngFor="let i of items" [ngValue]="i.id" class="form-control">{{i.name}}</option>
|
||||
</select>
|
||||
<ng-select name="inputId" [(ngModel)]="value"
|
||||
[disabled]="disabled"
|
||||
[style.color]="textColor"
|
||||
[style.background]="backgroundColor"
|
||||
[clearable]="allowNull"
|
||||
(change)="onChange(value)"
|
||||
(blur)="onTouched()">
|
||||
<ng-option *ngFor="let i of items" [value]="i.id">{{i.name}}</ng-option>
|
||||
</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">
|
||||
@@ -15,4 +20,4 @@
|
||||
</div>
|
||||
</div>
|
||||
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -0,0 +1 @@
|
||||
// styles for ng-select child are in styles.scss
|
||||
|
@@ -1,30 +1,43 @@
|
||||
<div class="form-group">
|
||||
<label for="exampleFormControlTextarea1">Tags</label>
|
||||
<div class="form-group paperless-input-select paperless-input-tags">
|
||||
<label for="tags" i18n>Tags</label>
|
||||
|
||||
<div class="input-group">
|
||||
<div class="form-control tags-form-control" id="tags">
|
||||
<app-tag class="mr-2" *ngFor="let id of displayValue" [tag]="getTag(id)" (click)="removeTag(id)"></app-tag>
|
||||
</div>
|
||||
<div class="input-group flex-nowrap">
|
||||
<ng-select name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="displayValue"
|
||||
[multiple]="true"
|
||||
[closeOnSelect]="false"
|
||||
[clearSearchOnAdd]="true"
|
||||
[disabled]="disabled"
|
||||
[hideSelected]="true"
|
||||
(change)="ngSelectChange()">
|
||||
|
||||
<div class="input-group-append" ngbDropdown placement="top-right">
|
||||
<button class="btn btn-outline-secondary" type="button" ngbDropdownToggle></button>
|
||||
<div ngbDropdownMenu class="scrollable-menu shadow">
|
||||
<button type="button" *ngFor="let tag of tags" ngbDropdownItem (click)="addTag(tag.id)">
|
||||
<app-tag [tag]="tag"></app-tag>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template ng-label-tmp let-item="item">
|
||||
<span class="tag-wrap tag-wrap-delete" (click)="removeTag(item.id)">
|
||||
<svg width="1.2em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg>
|
||||
<app-tag style="background-color: none;" [tag]="getTag(item.id)"></app-tag>
|
||||
</span>
|
||||
</ng-template>
|
||||
<ng-template ng-option-tmp let-item="item" let-index="index" let-search="searchTerm">
|
||||
<div class="tag-wrap">
|
||||
<div class="selected-icon d-inline-block mr-1">
|
||||
<svg *ngIf="displayValue.includes(item.id)" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#check"/>
|
||||
</svg>
|
||||
</div>
|
||||
<app-tag class="mr-2" [tag]="getTag(item.id)"></app-tag>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-select>
|
||||
|
||||
<div class="input-group-append">
|
||||
|
||||
<button class="btn btn-outline-secondary" type="button" (click)="createTag()">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<small class="form-text text-muted" *ngIf="hint">{{hint}}</small>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,10 +1,12 @@
|
||||
.tags-form-control {
|
||||
height: auto;
|
||||
.selected-icon {
|
||||
min-width: 1em;
|
||||
min-height: 1em;
|
||||
}
|
||||
|
||||
.tag-wrap {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.scrollable-menu {
|
||||
height: auto;
|
||||
max-height: 300px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.tag-wrap-delete {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
|
||||
|
||||
|
||||
onChange = (newValue: number[]) => {};
|
||||
|
||||
|
||||
onTouched = () => {};
|
||||
|
||||
writeValue(newValue: number[]): void {
|
||||
@@ -66,29 +66,28 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
|
||||
removeTag(id) {
|
||||
let index = this.displayValue.indexOf(id)
|
||||
if (index > -1) {
|
||||
this.displayValue.splice(index, 1)
|
||||
let oldValue = this.displayValue
|
||||
oldValue.splice(index, 1)
|
||||
this.displayValue = [...oldValue]
|
||||
this.onChange(this.displayValue)
|
||||
}
|
||||
}
|
||||
|
||||
addTag(id) {
|
||||
let index = this.displayValue.indexOf(id)
|
||||
if (index == -1) {
|
||||
this.displayValue.push(id)
|
||||
this.onChange(this.displayValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
createTag() {
|
||||
var modal = this.modalService.open(TagEditDialogComponent, {backdrop: 'static'})
|
||||
modal.componentInstance.dialogMode = 'create'
|
||||
modal.componentInstance.success.subscribe(newTag => {
|
||||
this.tagService.listAll().subscribe(tags => {
|
||||
this.tags = tags.results
|
||||
this.addTag(newTag.id)
|
||||
this.displayValue = [...this.displayValue, newTag.id]
|
||||
this.onChange(this.displayValue)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
ngSelectChange() {
|
||||
this.value = this.displayValue
|
||||
this.onChange(this.displayValue)
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,5 +1,8 @@
|
||||
<div class="form-group">
|
||||
<label [for]="inputId">{{title}}</label>
|
||||
<input type="text" class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)">
|
||||
<input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)">
|
||||
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
|
||||
<div class="invalid-feedback">
|
||||
{{error}}
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,15 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
|
||||
<button type="button" class="close" aria-label="Close" (click)="cancelClicked()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<app-input-select [items]="objects" [title]="message" [(ngModel)]="selected"></app-input-select>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-dark" (click)="cancelClicked()" i18n>Cancel</button>
|
||||
<button type="button" class="btn btn-primary" (click)="selectClicked.emit(selected)" i18n>Select</button>
|
||||
</div>
|
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SelectDialogComponent } from './select-dialog.component';
|
||||
|
||||
describe('SelectDialogComponent', () => {
|
||||
let component: SelectDialogComponent;
|
||||
let fixture: ComponentFixture<SelectDialogComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ SelectDialogComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SelectDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -0,0 +1,34 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ObjectWithId } from 'src/app/data/object-with-id';
|
||||
|
||||
@Component({
|
||||
selector: 'app-select-dialog',
|
||||
templateUrl: './select-dialog.component.html',
|
||||
styleUrls: ['./select-dialog.component.scss']
|
||||
})
|
||||
|
||||
export class SelectDialogComponent implements OnInit {
|
||||
constructor(public activeModal: NgbActiveModal) { }
|
||||
|
||||
@Output()
|
||||
public selectClicked = new EventEmitter()
|
||||
|
||||
@Input()
|
||||
title = $localize`Select`
|
||||
|
||||
@Input()
|
||||
message = $localize`Please select an object`
|
||||
|
||||
@Input()
|
||||
objects: ObjectWithId[] = []
|
||||
|
||||
selected: number
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
cancelClicked() {
|
||||
this.activeModal.close()
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
@@ -1,4 +1,5 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Meta } from '@angular/platform-browser';
|
||||
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
|
||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
|
||||
|
||||
@@ -11,8 +12,29 @@ import { SavedViewService } from 'src/app/services/rest/saved-view.service';
|
||||
export class DashboardComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private savedViewService: SavedViewService) { }
|
||||
private savedViewService: SavedViewService,
|
||||
private meta: Meta
|
||||
) { }
|
||||
|
||||
get displayName() {
|
||||
let tagFullName = this.meta.getTag('name=full_name')
|
||||
let tagUsername = this.meta.getTag('name=username')
|
||||
if (tagFullName && tagFullName.content) {
|
||||
return tagFullName.content
|
||||
} else if (tagUsername && tagUsername.content) {
|
||||
return tagUsername.content
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
get subtitle() {
|
||||
if (this.displayName) {
|
||||
return $localize`Hello ${this.displayName}, welcome to Paperless-ng!`
|
||||
} else {
|
||||
return $localize`Welcome to Paperless-ng!`
|
||||
}
|
||||
}
|
||||
|
||||
savedViews: PaperlessSavedView[] = []
|
||||
|
||||
|
@@ -1,13 +1,13 @@
|
||||
<app-widget-frame [title]="savedView.name">
|
||||
|
||||
<a header-buttons [routerLink]="" (click)="showAll()">Show all</a>
|
||||
<a header-buttons [routerLink]="" (click)="showAll()" i18n>Show all</a>
|
||||
|
||||
|
||||
<table content class="table table-sm table-hover table-borderless">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Created</th>
|
||||
<th scope="col">Title</th>
|
||||
<th i18n>Created</th>
|
||||
<th scope="col" i18n>Title</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@@ -0,0 +1,7 @@
|
||||
table {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
th:first-child {
|
||||
min-width: 5rem;
|
||||
}
|
||||
|
@@ -23,7 +23,7 @@ export class SavedViewWidgetComponent implements OnInit {
|
||||
documents: PaperlessDocument[] = []
|
||||
|
||||
ngOnInit(): void {
|
||||
this.documentService.list(1,10,this.savedView.sort_field, this.savedView.sort_reverse, this.savedView.filter_rules).subscribe(result => {
|
||||
this.documentService.listFiltered(1,10,this.savedView.sort_field, this.savedView.sort_reverse, this.savedView.filter_rules).subscribe(result => {
|
||||
this.documents = result.results
|
||||
})
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<app-widget-frame title="Statistics">
|
||||
<app-widget-frame title="Statistics" i18n-title>
|
||||
<ng-container content>
|
||||
<p class="card-text">Documents in inbox: {{statistics.documents_inbox}}</p>
|
||||
<p class="card-text">Total documents: {{statistics.documents_total}}</p>
|
||||
<p class="card-text" i18n>Documents in inbox: {{statistics.documents_inbox}}</p>
|
||||
<p class="card-text" i18n>Total documents: {{statistics.documents_total}}</p>
|
||||
</ng-container>
|
||||
</app-widget-frame>
|
@@ -1,16 +1,16 @@
|
||||
<app-widget-frame title="Upload new documents">
|
||||
<app-widget-frame title="Upload new documents" i18n-title>
|
||||
|
||||
<div content>
|
||||
<form>
|
||||
<ngx-file-drop dropZoneLabel="Drop documents here or" (onFileDrop)="dropped($event)"
|
||||
<ngx-file-drop dropZoneLabel="Drop documents here or" browseBtnLabel="Browse files" (onFileDrop)="dropped($event)"
|
||||
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" dropZoneClassName="bg-light card"
|
||||
multiple="true" contentClassName="justify-content-center d-flex align-items-center p-5" [showBrowseBtn]=true
|
||||
browseBtnClassName="btn btn-sm btn-outline-primary ml-2">
|
||||
browseBtnClassName="btn btn-sm btn-outline-primary ml-2" i18n-dropZoneLabel i18n-browseBtnLabel>
|
||||
|
||||
</ngx-file-drop>
|
||||
</form>
|
||||
<div *ngIf="uploadVisible" class="mt-3">
|
||||
<p>Uploading {{uploadStatus.length}} file(s)</p>
|
||||
<p i18n>{uploadStatus.length, plural, =1 {Uploading file...} =other {Uploading {{uploadStatus.length}} files...}}</p>
|
||||
<ngb-progressbar [value]="loadedSum" [max]="totalSum" [striped]="true" [animated]="uploadStatus.length > 0">
|
||||
</ngb-progressbar>
|
||||
</div>
|
||||
|
@@ -2,7 +2,7 @@ import { HttpEventType } from '@angular/common/http';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop';
|
||||
import { DocumentService } from 'src/app/services/rest/document.service';
|
||||
import { Toast, ToastService } from 'src/app/services/toast.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
|
||||
interface UploadStatus {
|
||||
@@ -60,7 +60,7 @@ export class UploadFileWidgetComponent implements OnInit {
|
||||
} else if (event.type == HttpEventType.Response) {
|
||||
this.uploadStatus.splice(this.uploadStatus.indexOf(uploadStatusObject), 1)
|
||||
this.completedFiles += 1
|
||||
this.toastService.showToast(Toast.make("Information", "The document has been uploaded and will be processed by the consumer shortly."))
|
||||
this.toastService.showInfo($localize`The document has been uploaded and will be processed by the consumer shortly.`)
|
||||
}
|
||||
|
||||
}, error => {
|
||||
@@ -68,11 +68,11 @@ export class UploadFileWidgetComponent implements OnInit {
|
||||
this.completedFiles += 1
|
||||
switch (error.status) {
|
||||
case 400: {
|
||||
this.toastService.showToast(Toast.makeError(`There was an error while uploading the document: ${error.error.document}`))
|
||||
this.toastService.showInfo($localize`There was an error while uploading the document: ${error.error.document}`)
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
this.toastService.showToast(Toast.makeError("An error has occurred while uploading the document. Sorry!"))
|
||||
this.toastService.showInfo($localize`An error has occurred while uploading the document. Sorry!`)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@@ -1,16 +1,16 @@
|
||||
<app-widget-frame title="First steps">
|
||||
<app-widget-frame title="First steps" i18n-title>
|
||||
|
||||
<ng-container content>
|
||||
<img src="assets/save-filter.png" class="float-right">
|
||||
<p>Paperless is running! :)</p>
|
||||
<p>You can start uploading documents by dropping them in the file upload box to the right or by dropping them in the configured consumption folder and they'll start showing up in the documents list.
|
||||
After you've added some metadata to your documents, use the filtering mechanisms of paperless to create custom views (such as 'Recently added', 'Tagged TODO') and have them displayed on the dashboard instead of this message.</p>
|
||||
<p>Paperless offers some more features that try to make your life easier, such as:</p>
|
||||
<p i18n>Paperless is running! :)</p>
|
||||
<p i18n>You can start uploading documents by dropping them in the file upload box to the right or by dropping them in the configured consumption folder and they'll start showing up in the documents list.
|
||||
After you've added some metadata to your documents, use the filtering mechanisms of paperless to create custom views (such as 'Recently added', 'Tagged TODO') and they will appear on the dashboard instead of this message.</p>
|
||||
<p i18n>Paperless offers some more features that try to make your life easier:</p>
|
||||
<ul>
|
||||
<li>Once you've got a couple documents in paperless and added metadata to them, paperless can assign that metadata to new documents automatically.</li>
|
||||
<li>You can configure paperless to read your mails and add documents from attached files.</li>
|
||||
<li i18n>Once you've got a couple documents in paperless and added metadata to them, paperless can assign that metadata to new documents automatically.</li>
|
||||
<li i18n>You can configure paperless to read your mails and add documents from attached files.</li>
|
||||
</ul>
|
||||
<p>Consult the documentation on how to use these features. The section on basic usage also has some information on how to use paperless in general.</p>
|
||||
<p i18n>Consult the documentation on how to use these features. The section on basic usage also has some information on how to use paperless in general.</p>
|
||||
</ng-container>
|
||||
|
||||
</app-widget-frame>
|
@@ -1,9 +1,18 @@
|
||||
<app-page-header [(title)]="title">
|
||||
<div class="input-group input-group-sm mr-5" *ngIf="getContentType() == 'application/pdf'">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text" i18n>Page</div>
|
||||
</div>
|
||||
<input class="form-control flex-grow-0 w-auto" type="number" min="1" [max]="previewNumPages" [(ngModel)]="previewCurrentPage" />
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text" i18n>of {{previewNumPages}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-outline-danger mr-2" (click)="delete()">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||
</svg>
|
||||
<span class="d-none d-lg-inline"> Delete</span>
|
||||
</svg> <span class="d-none d-lg-inline" i18n>Delete</span>
|
||||
</button>
|
||||
|
||||
<div class="btn-group mr-2">
|
||||
@@ -11,25 +20,28 @@
|
||||
<a [href]="downloadUrl" class="btn btn-sm btn-outline-primary">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#download" />
|
||||
</svg>
|
||||
<span class="d-none d-lg-inline"> Download</span>
|
||||
</svg> <span class="d-none d-lg-inline" i18n>Download</span>
|
||||
</a>
|
||||
|
||||
<div class="btn-group" ngbDropdown role="group" *ngIf="metadata?.has_archive_version">
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle-split" ngbDropdownToggle></button>
|
||||
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
||||
<a ngbDropdownItem [href]="downloadOriginalUrl">Download original</a>
|
||||
<a ngbDropdownItem [href]="downloadOriginalUrl" i18n>Download original</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-outline-primary mr-2" (click)="moreLike()">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#three-dots" />
|
||||
</svg> <span class="d-none d-lg-inline" i18n>More like this</span>
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="close()">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x" />
|
||||
</svg>
|
||||
<span class="d-none d-lg-inline"> Close</span>
|
||||
</svg> <span class="d-none d-lg-inline" i18n>Close</span>
|
||||
</button>
|
||||
</app-page-header>
|
||||
|
||||
@@ -41,27 +53,23 @@
|
||||
|
||||
<ul ngbNav #nav="ngbNav" class="nav-tabs">
|
||||
<li [ngbNavItem]="1">
|
||||
<a ngbNavLink>Details</a>
|
||||
<a ngbNavLink i18n>Details</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
<app-input-text title="Title" formControlName="title"></app-input-text>
|
||||
<div class="form-group">
|
||||
<label for="archive_serial_number">Archive Serial Number</label>
|
||||
<input type="number" class="form-control" id="archive_serial_number"
|
||||
formControlName='archive_serial_number'>
|
||||
</div>
|
||||
<app-input-date-time titleDate="Date created" formControlName="created"></app-input-date-time>
|
||||
<app-input-select [items]="correspondents" title="Correspondent" formControlName="correspondent"
|
||||
allowNull="true" (createNew)="createCorrespondent()"></app-input-select>
|
||||
<app-input-select [items]="documentTypes" title="Document type" formControlName="document_type"
|
||||
allowNull="true" (createNew)="createDocumentType()"></app-input-select>
|
||||
<app-input-tags formControlName="tags" title="Tags"></app-input-tags>
|
||||
<app-input-text #inputTitle i18n-title title="Title" formControlName="title" [error]="error?.title"></app-input-text>
|
||||
<app-input-number i18n-title title="Archive serial number" [error]="error?.archive_serial_number" formControlName='archive_serial_number'></app-input-number>
|
||||
<app-input-date-time i18n-titleDate titleDate="Date created" formControlName="created"></app-input-date-time>
|
||||
<app-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true"
|
||||
(createNew)="createCorrespondent()"></app-input-select>
|
||||
<app-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true"
|
||||
(createNew)="createDocumentType()"></app-input-select>
|
||||
<app-input-tags formControlName="tags"></app-input-tags>
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="2">
|
||||
<a ngbNavLink>Content</a>
|
||||
<a ngbNavLink i18n>Content</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="form-group">
|
||||
<textarea class="form-control" id="content" rows="20" formControlName='content'></textarea>
|
||||
@@ -70,48 +78,48 @@
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="3">
|
||||
<a ngbNavLink>Metadata</a>
|
||||
<a ngbNavLink i18n>Metadata</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
<table class="table table-borderless">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Date modified</td>
|
||||
<td i18n>Date modified</td>
|
||||
<td>{{document.modified | date:'medium'}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Date added</td>
|
||||
<td i18n>Date added</td>
|
||||
<td>{{document.added | date:'medium'}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Media filename</td>
|
||||
<td i18n>Media filename</td>
|
||||
<td>{{metadata?.media_filename}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Original MD5 Checksum</td>
|
||||
<td i18n>Original MD5 checksum</td>
|
||||
<td>{{metadata?.original_checksum}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Original file size</td>
|
||||
<td i18n>Original file size</td>
|
||||
<td>{{metadata?.original_size | fileSize}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Original mime type</td>
|
||||
<td i18n>Original mime type</td>
|
||||
<td>{{metadata?.original_mime_type}}</td>
|
||||
</tr>
|
||||
<tr *ngIf="metadata?.has_archive_version">
|
||||
<td>Archive MD5 Checksum</td>
|
||||
<td i18n>Archive MD5 checksum</td>
|
||||
<td>{{metadata?.archive_checksum}}</td>
|
||||
</tr>
|
||||
<tr *ngIf="metadata?.has_archive_version">
|
||||
<td>Archive file size</td>
|
||||
<td i18n>Archive file size</td>
|
||||
<td>{{metadata?.archive_size | fileSize}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<app-metadata-collapse title="Original document metadata" [metadata]="metadata.original_metadata" *ngIf="metadata?.original_metadata?.length > 0"></app-metadata-collapse>
|
||||
<app-metadata-collapse title="Archived document metadata" [metadata]="metadata.archive_metadata" *ngIf="metadata?.archive_metadata?.length > 0"></app-metadata-collapse>
|
||||
<app-metadata-collapse i18n-title title="Original document metadata" [metadata]="metadata.original_metadata" *ngIf="metadata?.original_metadata?.length > 0"></app-metadata-collapse>
|
||||
<app-metadata-collapse i18n-title title="Archived document metadata" [metadata]="metadata.archive_metadata" *ngIf="metadata?.archive_metadata?.length > 0"></app-metadata-collapse>
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
@@ -119,16 +127,15 @@
|
||||
|
||||
<div [ngbNavOutlet]="nav" class="mt-2"></div>
|
||||
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="discard()">Discard</button>
|
||||
<button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()">Save & edit
|
||||
next</button>
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="discard()" i18n [disabled]="networkActive">Discard</button>
|
||||
<button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()" i18n [disabled]="networkActive">Save & next</button>
|
||||
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-xl-8 mb-3">
|
||||
<div class="pdf-viewer-container" *ngIf="getContentType() == 'application/pdf'">
|
||||
<pdf-viewer [src]="previewUrl" [original-size]="false" [show-borders]="true"></pdf-viewer>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
@@ -15,6 +15,9 @@ import { DocumentService } from 'src/app/services/rest/document.service';
|
||||
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component';
|
||||
import { CorrespondentEditDialogComponent } from '../manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component';
|
||||
import { DocumentTypeEditDialogComponent } from '../manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component';
|
||||
import { PDFDocumentProxy } from 'ng2-pdf-viewer';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { TextComponent } from '../common/input/text/text.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-document-detail',
|
||||
@@ -23,8 +26,15 @@ import { DocumentTypeEditDialogComponent } from '../manage/document-type-list/do
|
||||
})
|
||||
export class DocumentDetailComponent implements OnInit {
|
||||
|
||||
public expandOriginalMetadata = false;
|
||||
public expandArchivedMetadata = false;
|
||||
@ViewChild("inputTitle")
|
||||
titleInput: TextComponent
|
||||
|
||||
expandOriginalMetadata = false
|
||||
expandArchivedMetadata = false
|
||||
|
||||
error: any
|
||||
|
||||
networkActive = false
|
||||
|
||||
documentId: number
|
||||
document: PaperlessDocument
|
||||
@@ -47,8 +57,11 @@ export class DocumentDetailComponent implements OnInit {
|
||||
tags: new FormControl([])
|
||||
})
|
||||
|
||||
previewCurrentPage: number = 1
|
||||
previewNumPages: number = 1
|
||||
|
||||
constructor(
|
||||
private documentsService: DocumentService,
|
||||
private documentsService: DocumentService,
|
||||
private route: ActivatedRoute,
|
||||
private correspondentService: CorrespondentService,
|
||||
private documentTypeService: DocumentTypeService,
|
||||
@@ -56,7 +69,8 @@ export class DocumentDetailComponent implements OnInit {
|
||||
private modalService: NgbModal,
|
||||
private openDocumentService: OpenDocumentsService,
|
||||
private documentListViewService: DocumentListViewService,
|
||||
private documentTitlePipe: DocumentTitlePipe) { }
|
||||
private documentTitlePipe: DocumentTitlePipe,
|
||||
private toastService: ToastService) { }
|
||||
|
||||
getContentType() {
|
||||
return this.metadata?.has_archive_version ? 'application/pdf' : this.metadata?.original_mime_type
|
||||
@@ -126,20 +140,35 @@ export class DocumentDetailComponent implements OnInit {
|
||||
}, error => {this.router.navigate(['404'])})
|
||||
}
|
||||
|
||||
save() {
|
||||
save() {
|
||||
this.networkActive = true
|
||||
this.documentsService.update(this.document).subscribe(result => {
|
||||
this.close()
|
||||
this.networkActive = false
|
||||
this.error = null
|
||||
}, error => {
|
||||
this.networkActive = false
|
||||
this.error = error.error
|
||||
})
|
||||
}
|
||||
|
||||
saveEditNext() {
|
||||
this.networkActive = true
|
||||
this.documentsService.update(this.document).subscribe(result => {
|
||||
this.error = null
|
||||
this.documentListViewService.getNext(this.document.id).subscribe(nextDocId => {
|
||||
this.networkActive = false
|
||||
if (nextDocId) {
|
||||
this.openDocumentService.closeDocument(this.document)
|
||||
this.router.navigate(['documents', nextDocId])
|
||||
this.titleInput.focus()
|
||||
}
|
||||
}, error => {
|
||||
this.networkActive = false
|
||||
})
|
||||
}, error => {
|
||||
this.networkActive = false
|
||||
this.error = error.error
|
||||
})
|
||||
}
|
||||
|
||||
@@ -154,21 +183,34 @@ export class DocumentDetailComponent implements OnInit {
|
||||
|
||||
delete() {
|
||||
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
||||
modal.componentInstance.title = "Confirm delete"
|
||||
modal.componentInstance.messageBold = `Do you really want to delete document '${this.document.title}'?`
|
||||
modal.componentInstance.message = `The files for this document will be deleted permanently. This operation cannot be undone.`
|
||||
modal.componentInstance.title = $localize`Confirm delete`
|
||||
modal.componentInstance.messageBold = $localize`Do you really want to delete document "${this.document.title}"?`
|
||||
modal.componentInstance.message = $localize`The files for this document will be deleted permanently. This operation cannot be undone.`
|
||||
modal.componentInstance.btnClass = "btn-danger"
|
||||
modal.componentInstance.btnCaption = "Delete document"
|
||||
modal.componentInstance.btnCaption = $localize`Delete document`
|
||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
this.documentsService.delete(this.document).subscribe(() => {
|
||||
modal.close()
|
||||
modal.close()
|
||||
this.close()
|
||||
}, error => {
|
||||
this.toastService.showError($localize`Error deleting document: ${JSON.stringify(error)}`)
|
||||
modal.componentInstance.buttonsEnabled = true
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
moreLike() {
|
||||
this.router.navigate(["search"], {queryParams: {more_like:this.document.id}})
|
||||
}
|
||||
|
||||
hasNext() {
|
||||
return this.documentListViewService.hasNext(this.documentId)
|
||||
}
|
||||
|
||||
pdfPreviewLoaded(pdf: PDFDocumentProxy) {
|
||||
this.previewNumPages = pdf.numPages
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -16,7 +16,7 @@
|
||||
<tbody>
|
||||
<tr *ngFor="let m of metadata">
|
||||
<td>{{m.prefix}}:{{m.key}}</td>
|
||||
<td>{{m.value}}</td>
|
||||
<td class="metadata-column">{{m.value}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@@ -0,0 +1,3 @@
|
||||
.metadata-column {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
@@ -15,7 +15,7 @@ export class MetadataCollapseComponent implements OnInit {
|
||||
metadata
|
||||
|
||||
@Input()
|
||||
title = "Metadata"
|
||||
title = $localize`Metadata`
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
@@ -0,0 +1,67 @@
|
||||
<div class="row">
|
||||
<div class="col-auto mb-2 mb-xl-0" role="group" aria-label="Select">
|
||||
<button class="btn btn-sm btn-outline-danger" (click)="list.selectNone()">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#slash-circle" />
|
||||
</svg> <ng-container i18n>Cancel</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
<div class="w-100 d-xl-none"></div>
|
||||
<div class="col-auto mb-2 mb-xl-0" role="group" aria-label="Select">
|
||||
<label class="mr-2 mb-0" i18n>Select:</label>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-outline-primary" (click)="list.selectPage()">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#file-earmark-check" />
|
||||
</svg> <ng-container i18n>Page</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary" (click)="list.selectAll()">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#check-all" />
|
||||
</svg> <ng-container i18n>All</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-100 d-xl-none"></div>
|
||||
<div class="col-auto mb-2 mb-xl-0">
|
||||
<div class="d-flex">
|
||||
<label class="ml-auto mt-1 mb-0 mr-2" i18n>Edit:</label>
|
||||
<app-filterable-dropdown class="mr-2 mr-md-3" title="Tags" icon="tag-fill" i18n-title
|
||||
filterPlaceholder="Filter tags" i18n-filterPlaceholder
|
||||
[items]="tags"
|
||||
[editing]="true"
|
||||
[multiple]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
(open)="openTagsDropdown()"
|
||||
[(selectionModel)]="tagSelectionModel"
|
||||
(apply)="setTags($event)">
|
||||
</app-filterable-dropdown>
|
||||
<app-filterable-dropdown class="mr-2 mr-md-3" title="Correspondent" icon="person-fill" i18n-title
|
||||
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
|
||||
[items]="correspondents"
|
||||
[editing]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
(open)="openCorrespondentDropdown()"
|
||||
[(selectionModel)]="correspondentSelectionModel"
|
||||
(apply)="setCorrespondents($event)">
|
||||
</app-filterable-dropdown>
|
||||
<app-filterable-dropdown class="mr-2 mr-md-3" title="Document type" icon="file-earmark-fill" i18n-title
|
||||
filterPlaceholder="Filter document types" i18n-filterPlaceholder
|
||||
[items]="documentTypes"
|
||||
[editing]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
(open)="openDocumentTypeDropdown()"
|
||||
[(selectionModel)]="documentTypeSelectionModel"
|
||||
(apply)="setDocumentTypes($event)">
|
||||
</app-filterable-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-100 d-xl-none"></div>
|
||||
<div class="col mb-2 mb-xl-0 d-flex">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger ml-0 ml-lg-auto" (click)="applyDelete()">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||
</svg> <ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BulkEditorComponent } from './bulk-editor.component';
|
||||
|
||||
describe('BulkEditorComponent', () => {
|
||||
let component: BulkEditorComponent;
|
||||
let fixture: ComponentFixture<BulkEditorComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ BulkEditorComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BulkEditorComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -0,0 +1,210 @@
|
||||
import { Component } 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';
|
||||
import { TagService } from 'src/app/services/rest/tag.service';
|
||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
|
||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service';
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { DocumentService, SelectionDataItem } from 'src/app/services/rest/document.service';
|
||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service';
|
||||
import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component';
|
||||
import { ChangedItems, FilterableDropdownSelectionModel } from '../../common/filterable-dropdown/filterable-dropdown.component';
|
||||
import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component';
|
||||
import { MatchingModel } from 'src/app/data/matching-model';
|
||||
import { SettingsService, SETTINGS_KEYS } from 'src/app/services/settings.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bulk-editor',
|
||||
templateUrl: './bulk-editor.component.html',
|
||||
styleUrls: ['./bulk-editor.component.scss']
|
||||
})
|
||||
export class BulkEditorComponent {
|
||||
|
||||
tags: PaperlessTag[]
|
||||
correspondents: PaperlessCorrespondent[]
|
||||
documentTypes: PaperlessDocumentType[]
|
||||
|
||||
tagSelectionModel = new FilterableDropdownSelectionModel()
|
||||
correspondentSelectionModel = new FilterableDropdownSelectionModel()
|
||||
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
|
||||
|
||||
constructor(
|
||||
private documentTypeService: DocumentTypeService,
|
||||
private tagService: TagService,
|
||||
private correspondentService: CorrespondentService,
|
||||
public list: DocumentListViewService,
|
||||
private documentService: DocumentService,
|
||||
private modalService: NgbModal,
|
||||
private openDocumentService: OpenDocumentsService,
|
||||
private settings: SettingsService,
|
||||
private toastService: ToastService
|
||||
) { }
|
||||
|
||||
applyOnClose: boolean = this.settings.get(SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE)
|
||||
showConfirmationDialogs: boolean = this.settings.get(SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS)
|
||||
|
||||
ngOnInit() {
|
||||
this.tagService.listAll().subscribe(result => this.tags = result.results)
|
||||
this.correspondentService.listAll().subscribe(result => this.correspondents = result.results)
|
||||
this.documentTypeService.listAll().subscribe(result => this.documentTypes = result.results)
|
||||
}
|
||||
|
||||
private executeBulkOperation(modal, method: string, args) {
|
||||
if (modal) {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
}
|
||||
this.documentService.bulkEdit(Array.from(this.list.selected), method, args).subscribe(
|
||||
response => {
|
||||
this.list.reload()
|
||||
this.list.reduceSelectionToFilter()
|
||||
this.list.selected.forEach(id => {
|
||||
this.openDocumentService.refreshDocument(id)
|
||||
})
|
||||
if (modal) {
|
||||
modal.close()
|
||||
}
|
||||
}, error => {
|
||||
if (modal) {
|
||||
modal.componentInstance.buttonsEnabled = true
|
||||
}
|
||||
this.toastService.showError($localize`Error executing bulk operation: ${JSON.stringify(error.error)}`)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private applySelectionData(items: SelectionDataItem[], selectionModel: FilterableDropdownSelectionModel) {
|
||||
let selectionData = new Map<number, ToggleableItemState>()
|
||||
items.forEach(i => {
|
||||
if (i.document_count == this.list.selected.size) {
|
||||
selectionData.set(i.id, ToggleableItemState.Selected)
|
||||
} else if (i.document_count > 0) {
|
||||
selectionData.set(i.id, ToggleableItemState.PartiallySelected)
|
||||
}
|
||||
})
|
||||
selectionModel.init(selectionData)
|
||||
}
|
||||
|
||||
openTagsDropdown() {
|
||||
this.documentService.getSelectionData(Array.from(this.list.selected)).subscribe(s => {
|
||||
this.applySelectionData(s.selected_tags, this.tagSelectionModel)
|
||||
})
|
||||
}
|
||||
|
||||
openDocumentTypeDropdown() {
|
||||
this.documentService.getSelectionData(Array.from(this.list.selected)).subscribe(s => {
|
||||
this.applySelectionData(s.selected_document_types, this.documentTypeSelectionModel)
|
||||
})
|
||||
}
|
||||
|
||||
openCorrespondentDropdown() {
|
||||
this.documentService.getSelectionData(Array.from(this.list.selected)).subscribe(s => {
|
||||
this.applySelectionData(s.selected_correspondents, this.correspondentSelectionModel)
|
||||
})
|
||||
}
|
||||
|
||||
private _localizeList(items: MatchingModel[]) {
|
||||
if (items.length == 0) {
|
||||
return ""
|
||||
} else if (items.length == 1) {
|
||||
return items[0].name
|
||||
} else if (items.length == 2) {
|
||||
return $localize`:This is for messages like 'modify "tag1" and "tag2"':"${items[0].name}" and "${items[1].name}"`
|
||||
} else {
|
||||
let list = items.slice(0, items.length - 1).map(i => $localize`"${i.name}"`).join($localize`:this is used to separate enumerations and should probably be a comma and a whitespace in most languages:, `)
|
||||
return $localize`:this is for messages like 'modify "tag1", "tag2" and "tag3"':${list} and "${items[items.length - 1].name}"`
|
||||
}
|
||||
}
|
||||
|
||||
setTags(changedTags: ChangedItems) {
|
||||
if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length == 0) return
|
||||
|
||||
if (this.showConfirmationDialogs) {
|
||||
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
||||
modal.componentInstance.title = $localize`Confirm tags assignment`
|
||||
if (changedTags.itemsToAdd.length == 1 && changedTags.itemsToRemove.length == 0) {
|
||||
let tag = changedTags.itemsToAdd[0]
|
||||
modal.componentInstance.message = $localize`This operation will add the tag "${tag.name}" to ${this.list.selected.size} selected document(s).`
|
||||
} else if (changedTags.itemsToAdd.length > 1 && changedTags.itemsToRemove.length == 0) {
|
||||
modal.componentInstance.message = $localize`This operation will add the tags ${this._localizeList(changedTags.itemsToAdd)} to ${this.list.selected.size} selected document(s).`
|
||||
} else if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length == 1) {
|
||||
let tag = changedTags.itemsToRemove[0]
|
||||
modal.componentInstance.message = $localize`This operation will remove the tag "${tag.name}" from ${this.list.selected.size} selected document(s).`
|
||||
} else if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length > 1) {
|
||||
modal.componentInstance.message = $localize`This operation will remove the tags ${this._localizeList(changedTags.itemsToRemove)} from ${this.list.selected.size} selected document(s).`
|
||||
} else {
|
||||
modal.componentInstance.message = $localize`This operation will add the tags ${this._localizeList(changedTags.itemsToAdd)} and remove the tags ${this._localizeList(changedTags.itemsToRemove)} on ${this.list.selected.size} selected document(s).`
|
||||
}
|
||||
|
||||
modal.componentInstance.btnClass = "btn-warning"
|
||||
modal.componentInstance.btnCaption = $localize`Confirm`
|
||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||
this.executeBulkOperation(modal, 'modify_tags', {"add_tags": changedTags.itemsToAdd.map(t => t.id), "remove_tags": changedTags.itemsToRemove.map(t => t.id)})
|
||||
})
|
||||
} else {
|
||||
this.executeBulkOperation(null, 'modify_tags', {"add_tags": changedTags.itemsToAdd.map(t => t.id), "remove_tags": changedTags.itemsToRemove.map(t => t.id)})
|
||||
}
|
||||
}
|
||||
|
||||
setCorrespondents(changedCorrespondents: ChangedItems) {
|
||||
if (changedCorrespondents.itemsToAdd.length == 0 && changedCorrespondents.itemsToRemove.length == 0) return
|
||||
|
||||
let correspondent = changedCorrespondents.itemsToAdd.length > 0 ? changedCorrespondents.itemsToAdd[0] : null
|
||||
|
||||
if (this.showConfirmationDialogs) {
|
||||
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
||||
modal.componentInstance.title = $localize`Confirm correspondent assignment`
|
||||
if (correspondent) {
|
||||
modal.componentInstance.message = $localize`This operation will assign the correspondent "${correspondent.name}" to ${this.list.selected.size} selected document(s).`
|
||||
} else {
|
||||
modal.componentInstance.message = $localize`This operation will remove the correspondent from ${this.list.selected.size} selected document(s).`
|
||||
}
|
||||
modal.componentInstance.btnClass = "btn-warning"
|
||||
modal.componentInstance.btnCaption = $localize`Confirm`
|
||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||
this.executeBulkOperation(modal, 'set_correspondent', {"correspondent": correspondent ? correspondent.id : null})
|
||||
})
|
||||
} else {
|
||||
this.executeBulkOperation(null, 'set_correspondent', {"correspondent": correspondent ? correspondent.id : null})
|
||||
}
|
||||
}
|
||||
|
||||
setDocumentTypes(changedDocumentTypes: ChangedItems) {
|
||||
if (changedDocumentTypes.itemsToAdd.length == 0 && changedDocumentTypes.itemsToRemove.length == 0) return
|
||||
|
||||
let documentType = changedDocumentTypes.itemsToAdd.length > 0 ? changedDocumentTypes.itemsToAdd[0] : null
|
||||
|
||||
if (this.showConfirmationDialogs) {
|
||||
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
||||
modal.componentInstance.title = $localize`Confirm document type assignment`
|
||||
if (documentType) {
|
||||
modal.componentInstance.message = $localize`This operation will assign the document type "${documentType.name}" to ${this.list.selected.size} selected document(s).`
|
||||
} else {
|
||||
modal.componentInstance.message = $localize`This operation will remove the document type from ${this.list.selected.size} selected document(s).`
|
||||
}
|
||||
modal.componentInstance.btnClass = "btn-warning"
|
||||
modal.componentInstance.btnCaption = $localize`Confirm`
|
||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||
this.executeBulkOperation(modal, 'set_document_type', {"document_type": documentType ? documentType.id : null})
|
||||
})
|
||||
} else {
|
||||
this.executeBulkOperation(null, 'set_document_type', {"document_type": documentType ? documentType.id : null})
|
||||
}
|
||||
}
|
||||
|
||||
applyDelete() {
|
||||
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
||||
modal.componentInstance.delayConfirm(5)
|
||||
modal.componentInstance.title = $localize`Delete confirm`
|
||||
modal.componentInstance.messageBold = $localize`This operation will permanently delete ${this.list.selected.size} selected document(s).`
|
||||
modal.componentInstance.message = $localize`This operation cannot be undone.`
|
||||
modal.componentInstance.btnClass = "btn-danger"
|
||||
modal.componentInstance.btnCaption = $localize`Delete document(s)`
|
||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
this.executeBulkOperation(modal, "delete", {})
|
||||
})
|
||||
}
|
||||
}
|
@@ -1,19 +1,27 @@
|
||||
<div class="card mb-3 bg-light shadow-sm">
|
||||
<div class="card mb-3 shadow-sm" [class.card-selected]="selected" [class.document-card]="selectable">
|
||||
<div class="row no-gutters">
|
||||
<div class="col-md-2 d-none d-lg-block">
|
||||
<img [src]="getThumbUrl()" class="card-img doc-img border-right">
|
||||
<div class="col-md-2 d-none d-lg-block doc-img-background rounded-left" [class.doc-img-background-selected]="selected">
|
||||
<img [src]="getThumbUrl()" class="card-img doc-img border-right rounded-left" (click)="setSelected(selectable ? !selected : false)">
|
||||
|
||||
<div style="top: 0; left: 0" class="position-absolute border-right border-bottom bg-light p-1" [class.document-card-check]="!selected">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="smallCardCheck{{document.id}}" [checked]="selected" (change)="setSelected($event.target.checked)">
|
||||
<label class="custom-control-label" for="smallCardCheck{{document.id}}"></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card-body">
|
||||
<div class="card-body bg-light">
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title">
|
||||
<ng-container *ngIf="document.correspondent">
|
||||
<a *ngIf="clickCorrespondent.observers.length ; else nolink" [routerLink]="" title="Filter by correspondent" (click)="clickCorrespondent.emit(document.correspondent)" class="font-weight-bold">{{(document.correspondent$ | async)?.name}}</a>
|
||||
<a *ngIf="clickCorrespondent.observers.length ; else nolink" [routerLink]="" title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent)" class="font-weight-bold">{{(document.correspondent$ | async)?.name}}</a>
|
||||
<ng-template #nolink>{{(document.correspondent$ | async)?.name}}</ng-template>:
|
||||
</ng-container>
|
||||
{{document.title | documentTitle}}
|
||||
<app-tag [tag]="t" linkTitle="Filter by tag" *ngFor="let t of document.tags$ | async" class="ml-1" (click)="clickTag.emit(t.id)" [clickable]="clickTag.observers.length"></app-tag>
|
||||
<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)" [clickable]="clickTag.observers.length"></app-tag>
|
||||
</h5>
|
||||
<h5 class="card-title" *ngIf="document.archive_serial_number">#{{document.archive_serial_number}}</h5>
|
||||
</div>
|
||||
@@ -23,30 +31,38 @@
|
||||
</p>
|
||||
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="btn-group">
|
||||
<a routerLink="/search" [queryParams]="{'more_like': document.id}" class="btn btn-sm btn-outline-secondary" *ngIf="moreLikeThis">
|
||||
<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> <ng-container i18n>More like this</ng-container>
|
||||
</a>
|
||||
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary">
|
||||
<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>
|
||||
Edit
|
||||
</svg> <ng-container i18n>Edit</ng-container>
|
||||
</a>
|
||||
<a type="button" class="btn btn-sm btn-outline-secondary" [href]="getPreviewUrl()">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-search" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M10.442 10.442a1 1 0 0 1 1.415 0l3.85 3.85a1 1 0 0 1-1.414 1.415l-3.85-3.85a1 1 0 0 1 0-1.415z"/>
|
||||
<path fill-rule="evenodd" d="M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0z"/>
|
||||
</svg>
|
||||
View
|
||||
</svg> <ng-container i18n>View</ng-container>
|
||||
</a>
|
||||
<a type="button" 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>
|
||||
Download
|
||||
</svg> <ng-container i18n>Download</ng-container>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
<small class="text-muted">Created: {{document.created | date}}</small>
|
||||
|
||||
<small *ngIf="searchScore" class="text-muted ml-auto" i18n>Score:</small>
|
||||
|
||||
<ngb-progressbar *ngIf="searchScore" [type]="searchScoreClass" [value]="searchScore" class="search-score-bar mx-2" [max]="1"></ngb-progressbar>
|
||||
|
||||
<small class="text-muted" [class.ml-auto]="!searchScore" i18n>Created: {{document.created | date}}</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@@ -1,5 +1,6 @@
|
||||
@import "/src/theme";
|
||||
|
||||
.result-content {
|
||||
color: darkgray;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@@ -8,5 +9,27 @@
|
||||
object-position: top;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
||||
}
|
||||
.search-score-bar {
|
||||
width: 100px;
|
||||
height: 5px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.document-card-check {
|
||||
display: none
|
||||
}
|
||||
|
||||
.document-card:hover .document-card-check {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.card-selected {
|
||||
border-color: $primary;
|
||||
}
|
||||
|
||||
.doc-img-background-selected {
|
||||
background-color: $primaryFaded;
|
||||
}
|
||||
|
@@ -12,6 +12,24 @@ export class DocumentCardLargeComponent implements OnInit {
|
||||
|
||||
constructor(private documentService: DocumentService, private sanitizer: DomSanitizer) { }
|
||||
|
||||
@Input()
|
||||
selected = false
|
||||
|
||||
setSelected(value: boolean) {
|
||||
this.selected = value
|
||||
this.selectedChange.emit(value)
|
||||
}
|
||||
|
||||
@Output()
|
||||
selectedChange = new EventEmitter<boolean>()
|
||||
|
||||
get selectable() {
|
||||
return this.selectedChange.observers.length > 0
|
||||
}
|
||||
|
||||
@Input()
|
||||
moreLikeThis: boolean = false
|
||||
|
||||
@Input()
|
||||
document: PaperlessDocument
|
||||
|
||||
@@ -24,6 +42,19 @@ export class DocumentCardLargeComponent implements OnInit {
|
||||
@Output()
|
||||
clickCorrespondent = new EventEmitter<number>()
|
||||
|
||||
@Input()
|
||||
searchScore: number
|
||||
|
||||
get searchScoreClass() {
|
||||
if (this.searchScore > 0.7) {
|
||||
return "success"
|
||||
} else if (this.searchScore > 0.3) {
|
||||
return "warning"
|
||||
} else {
|
||||
return "danger"
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
|
@@ -1,10 +1,18 @@
|
||||
<div class="col p-2 h-100" style="width: 16rem;">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="border-bottom">
|
||||
<img class="card-img doc-img" [src]="getThumbUrl()">
|
||||
<div class="col p-2 h-100">
|
||||
<div class="card h-100 shadow-sm document-card" [class.card-selected]="selected">
|
||||
<div class="border-bottom doc-img-container" [class.doc-img-background-selected]="selected">
|
||||
<img class="card-img doc-img rounded-top" [src]="getThumbUrl()" (click)="setSelected(!selected)">
|
||||
|
||||
<div class="border-right border-bottom bg-light p-1 rounded document-card-check">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="smallCardCheck{{document.id}}" [checked]="selected" (change)="setSelected($event.target.checked)">
|
||||
<label class="custom-control-label" for="smallCardCheck{{document.id}}"></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="top: 0; right: 0; font-size: large" class="text-right position-absolute mr-1">
|
||||
<div *ngFor="let t of getTagsLimited$() | async">
|
||||
<app-tag [tag]="t" (click)="clickTag.emit(t.id)" [clickable]="true" linkTitle="Filter by tag"></app-tag>
|
||||
<app-tag [tag]="t" (click)="clickTag.emit(t.id)" [clickable]="true" linkTitle="Filter by tag" i18n-linkTitle></app-tag>
|
||||
</div>
|
||||
<div *ngIf="moreTags">
|
||||
<span class="badge badge-secondary">+ {{moreTags}}</span>
|
||||
@@ -15,34 +23,34 @@
|
||||
<div class="card-body p-2">
|
||||
<p class="card-text">
|
||||
<ng-container *ngIf="document.correspondent">
|
||||
<a [routerLink]="" title="Filter by correspondent" (click)="clickCorrespondent.emit(document.correspondent)" class="font-weight-bold">{{(document.correspondent$ | async)?.name}}</a>:
|
||||
<a [routerLink]="" title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent)" class="font-weight-bold">{{(document.correspondent$ | async)?.name}}</a>:
|
||||
</ng-container>
|
||||
{{document.title | documentTitle}}
|
||||
{{document.title | documentTitle}} <span *ngIf="document.archive_serial_number">(#{{document.archive_serial_number}})</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center ml-n2">
|
||||
<div class="d-flex justify-content-between align-items-center mx-n2">
|
||||
<div class="btn-group">
|
||||
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" title="Edit">
|
||||
<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">
|
||||
<a [href]="getPreviewUrl()" class="btn btn-sm btn-outline-secondary" title="View in browser" i18n-title>
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-search" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M10.442 10.442a1 1 0 0 1 1.415 0l3.85 3.85a1 1 0 0 1-1.414 1.415l-3.85-3.85a1 1 0 0 1 0-1.415z"/>
|
||||
<path fill-rule="evenodd" d="M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a [href]="getDownloadUrl()" class="btn btn-sm btn-outline-secondary" title="Download">
|
||||
<a [href]="getDownloadUrl()" class="btn btn-sm btn-outline-secondary" title="Download" 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"/>
|
||||
<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>
|
||||
</a>
|
||||
</div>
|
||||
<small class="text-muted">{{document.created | date}}</small>
|
||||
<small class="text-muted pl-1">{{document.created | date}}</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@@ -1,5 +1,36 @@
|
||||
@import "/src/theme";
|
||||
|
||||
.doc-img {
|
||||
object-fit: cover;
|
||||
object-position: top;
|
||||
height: 200px;
|
||||
}
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
||||
.document-card-check {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
.custom-control {
|
||||
margin-left: 4px;
|
||||
margin-right: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
.document-card:hover .document-card-check {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.card-selected {
|
||||
border-color: $primary;
|
||||
|
||||
.document-card-check {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.doc-img-background-selected {
|
||||
background-color: $primaryFaded;
|
||||
}
|
||||
|
@@ -12,6 +12,17 @@ export class DocumentCardSmallComponent implements OnInit {
|
||||
|
||||
constructor(private documentService: DocumentService) { }
|
||||
|
||||
@Input()
|
||||
selected = false
|
||||
|
||||
setSelected(value: boolean) {
|
||||
this.selected = value
|
||||
this.selectedChange.emit(value)
|
||||
}
|
||||
|
||||
@Output()
|
||||
selectedChange = new EventEmitter<boolean>()
|
||||
|
||||
@Input()
|
||||
document: PaperlessDocument
|
||||
|
||||
|
@@ -1,4 +1,19 @@
|
||||
<app-page-header [title]="getTitle()">
|
||||
|
||||
<div ngbDropdown class="d-inline-block mr-2">
|
||||
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#text-indent-left" />
|
||||
</svg> <ng-container i18n>Select</ng-container>
|
||||
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
|
||||
<button ngbDropdownItem (click)="list.selectNone()" i18n>Select none</button>
|
||||
<button ngbDropdownItem (click)="list.selectPage()" i18n>Select page</button>
|
||||
<button ngbDropdownItem (click)="list.selectAll()" i18n>Select all</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group btn-group-toggle" ngbRadioGroup [(ngModel)]="displayMode"
|
||||
(ngModelChange)="saveDisplayMode()">
|
||||
<label ngbButtonLabel class="btn-outline-primary btn-sm">
|
||||
@@ -23,7 +38,7 @@
|
||||
|
||||
<div class="btn-group btn-group-toggle ml-2" ngbRadioGroup [(ngModel)]="list.sortReverse">
|
||||
<div ngbDropdown class="btn-group">
|
||||
<button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle>Sort by</button>
|
||||
<button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle i18n>Sort by</button>
|
||||
<div ngbDropdownMenu aria-labelledby="dropdownBasic1" class="shadow">
|
||||
<button *ngFor="let f of getSortFields()" ngbDropdownItem (click)="list.sortField = f.field"
|
||||
[class.active]="list.sortField == f.field">{{f.name}}</button>
|
||||
@@ -46,15 +61,15 @@
|
||||
<div class="btn-group ml-2">
|
||||
|
||||
<div class="btn-group" ngbDropdown role="group">
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle" ngbDropdownToggle>Views</button>
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle" ngbDropdownToggle i18n>Views</button>
|
||||
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
||||
<ng-container *ngIf="!list.savedViewId">
|
||||
<button ngbDropdownItem *ngFor="let view of savedViewService.allViews" (click)="loadViewConfig(view)">{{view.name}}</button>
|
||||
<div class="dropdown-divider" *ngIf="savedViewService.allViews.length > 0"></div>
|
||||
</ng-container>
|
||||
|
||||
<button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.savedViewId">Save "{{list.savedViewTitle}}"</button>
|
||||
<button ngbDropdownItem (click)="saveViewConfigAs()">Save as...</button>
|
||||
<button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.savedViewId" i18n>Save "{{list.savedViewTitle}}"</button>
|
||||
<button ngbDropdownItem (click)="saveViewConfigAs()" i18n>Save as...</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -63,31 +78,72 @@
|
||||
</app-page-header>
|
||||
|
||||
<div class="w-100 mb-2 mb-sm-4">
|
||||
<app-filter-editor [(filterRules)]="list.filterRules" #filterEditor></app-filter-editor>
|
||||
<app-filter-editor [hidden]="isBulkEditing" [(filterRules)]="list.filterRules" [rulesModified]="filterRulesModified" (filterRulesChange)="rulesChanged()" (reset)="resetFilters()" #filterEditor></app-filter-editor>
|
||||
<app-bulk-editor [hidden]="!isBulkEditing"></app-bulk-editor>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<p>{{list.collectionSize || 0}} document(s) <span *ngIf="isFiltered">(filtered)</span></p>
|
||||
<p>
|
||||
<span i18n *ngIf="list.selected.size > 0">{list.collectionSize, plural, =1 {Selected {{list.selected.size}} of one document} other {Selected {{list.selected.size}} of {{list.collectionSize || 0}} documents}}</span>
|
||||
<span i18n *ngIf="list.selected.size == 0">{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</span> <span i18n *ngIf="isFiltered">(filtered)</span>
|
||||
</p>
|
||||
<ngb-pagination [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
|
||||
[rotate]="true" (pageChange)="list.reload()" aria-label="Default pagination"></ngb-pagination>
|
||||
</div>
|
||||
|
||||
<div *ngIf="displayMode == 'largeCards'">
|
||||
<app-document-card-large *ngFor="let d of list.documents" [document]="d" [details]="d.content" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)">
|
||||
<app-document-card-large [selected]="list.isSelected(d)" (selectedChange)="list.setSelected(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>
|
||||
|
||||
<table class="table table-sm border shadow-sm" *ngIf="displayMode == 'details'">
|
||||
<thead>
|
||||
<th class="d-none d-lg-table-cell">ASN</th>
|
||||
<th class="d-none d-md-table-cell">Correspondent</th>
|
||||
<th>Title</th>
|
||||
<th class="d-none d-xl-table-cell">Document type</th>
|
||||
<th>Created</th>
|
||||
<th class="d-none d-xl-table-cell">Added</th>
|
||||
<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">
|
||||
<tr *ngFor="let d of list.documents; trackBy: trackByDocumentId" [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)" (change)="list.setSelected(d, $event.target.checked)">
|
||||
<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>
|
||||
@@ -115,7 +171,6 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<div class=" m-n2 row" *ngIf="displayMode == 'smallCards'">
|
||||
<app-document-card-small [document]="d" *ngFor="let d of list.documents" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)"></app-document-card-small>
|
||||
<div class="m-n2 row row-cols-paperless-cards" *ngIf="displayMode == 'smallCards'">
|
||||
<app-document-card-small [selected]="list.isSelected(d)" (selectedChange)="list.setSelected(d, $event)" [document]="d" *ngFor="let d of list.documents; trackBy: trackByDocumentId" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)"></app-document-card-small>
|
||||
</div>
|
||||
|
@@ -0,0 +1,27 @@
|
||||
@import "/src/theme";
|
||||
|
||||
.table-row-selected {
|
||||
background-color: $primaryFaded;
|
||||
}
|
||||
|
||||
$paperless-card-breakpoints: (
|
||||
0: 2, // xs
|
||||
768px: 3, //md
|
||||
992px: 4, //lg
|
||||
1200px: 5, //xl
|
||||
1400px: 6, // xxl
|
||||
1600px: 7,
|
||||
1800px: 8,
|
||||
2000px: 9
|
||||
);
|
||||
|
||||
.row-cols-paperless-cards {
|
||||
@each $width, $n_cols in $paperless-card-breakpoints {
|
||||
@media(min-width: $width) {
|
||||
> * {
|
||||
flex: 0 0 auto;
|
||||
width: 100% / $n_cols;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,13 +1,14 @@
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { AfterViewInit, Component, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { FILTER_CORRESPONDENT } 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 { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
||||
import { DOCUMENT_SORT_FIELDS } from 'src/app/services/rest/document.service';
|
||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
|
||||
import { Toast, ToastService } from 'src/app/services/toast.service';
|
||||
import { FilterEditorComponent } from '../filter-editor/filter-editor.component';
|
||||
import { FilterEditorComponent } from './filter-editor/filter-editor.component';
|
||||
import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component';
|
||||
|
||||
@Component({
|
||||
@@ -23,25 +24,37 @@ export class DocumentListComponent implements OnInit {
|
||||
public route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private toastService: ToastService,
|
||||
public modalService: NgbModal) { }
|
||||
private modalService: NgbModal) { }
|
||||
|
||||
@ViewChild("filterEditor")
|
||||
private filterEditor: FilterEditorComponent
|
||||
|
||||
@ViewChildren(SortableDirective) headers: QueryList<SortableDirective>;
|
||||
|
||||
displayMode = 'smallCards' // largeCards, smallCards, details
|
||||
|
||||
filterRulesModified: boolean = false
|
||||
|
||||
get isFiltered() {
|
||||
return this.list.filterRules?.length > 0
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
return this.list.savedViewTitle || "Documents"
|
||||
return this.list.savedViewTitle || $localize`Documents`
|
||||
}
|
||||
|
||||
getSortFields() {
|
||||
return DOCUMENT_SORT_FIELDS
|
||||
}
|
||||
|
||||
onSort(event: SortEvent) {
|
||||
this.list.setSort(event.column, event.reverse)
|
||||
}
|
||||
|
||||
get isBulkEditing(): boolean {
|
||||
return this.list.selected.size > 0
|
||||
}
|
||||
|
||||
saveDisplayMode() {
|
||||
localStorage.setItem('document-list:displayMode', this.displayMode)
|
||||
}
|
||||
@@ -58,26 +71,27 @@ export class DocumentListComponent implements OnInit {
|
||||
this.router.navigate(["404"])
|
||||
return
|
||||
}
|
||||
|
||||
this.list.savedView = view
|
||||
this.list.reload()
|
||||
this.rulesChanged()
|
||||
})
|
||||
} else {
|
||||
this.list.savedView = null
|
||||
this.list.reload()
|
||||
this.rulesChanged()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
loadViewConfig(view: PaperlessSavedView) {
|
||||
this.list.load(view)
|
||||
this.list.reload()
|
||||
this.rulesChanged()
|
||||
}
|
||||
|
||||
saveViewConfig() {
|
||||
this.savedViewService.update(this.list.savedView).subscribe(result => {
|
||||
this.toastService.showToast(Toast.make("Information", `View "${this.list.savedView.name}" saved successfully.`))
|
||||
this.toastService.showInfo($localize`View "${this.list.savedView.name}" saved successfully.`)
|
||||
})
|
||||
|
||||
}
|
||||
@@ -86,6 +100,7 @@ export class DocumentListComponent implements OnInit {
|
||||
let modal = this.modalService.open(SaveViewConfigDialogComponent, {backdrop: 'static'})
|
||||
modal.componentInstance.defaultName = this.filterEditor.generateFilterName()
|
||||
modal.componentInstance.saveClicked.subscribe(formValue => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
let savedView = {
|
||||
name: formValue.name,
|
||||
show_on_dashboard: formValue.showOnDashboard,
|
||||
@@ -94,23 +109,79 @@ export class DocumentListComponent implements OnInit {
|
||||
sort_reverse: this.list.sortReverse,
|
||||
sort_field: this.list.sortField
|
||||
}
|
||||
|
||||
this.savedViewService.create(savedView).subscribe(() => {
|
||||
modal.close()
|
||||
this.toastService.showToast(Toast.make("Information", `View "${savedView.name}" created successfully.`))
|
||||
this.toastService.showInfo($localize`View "${savedView.name}" created successfully.`)
|
||||
}, error => {
|
||||
modal.componentInstance.error = error.error
|
||||
modal.componentInstance.buttonsEnabled = true
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
resetFilters(): void {
|
||||
this.filterRulesModified = false
|
||||
if (this.list.savedViewId) {
|
||||
this.savedViewService.getCached(this.list.savedViewId).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.savedView == 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.savedViewId).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
|
||||
}
|
||||
|
||||
clickTag(tagID: number) {
|
||||
this.filterEditor.toggleTag(tagID)
|
||||
this.list.selectNone()
|
||||
setTimeout(() => {
|
||||
this.filterEditor.toggleTag(tagID)
|
||||
})
|
||||
}
|
||||
|
||||
clickCorrespondent(correspondentID: number) {
|
||||
this.filterEditor.toggleCorrespondent(correspondentID)
|
||||
this.list.selectNone()
|
||||
setTimeout(() => {
|
||||
this.filterEditor.toggleCorrespondent(correspondentID)
|
||||
})
|
||||
}
|
||||
|
||||
clickDocumentType(documentTypeID: number) {
|
||||
this.filterEditor.toggleDocumentType(documentTypeID)
|
||||
this.list.selectNone()
|
||||
setTimeout(() => {
|
||||
this.filterEditor.toggleDocumentType(documentTypeID)
|
||||
})
|
||||
}
|
||||
|
||||
trackByDocumentId(index, item: PaperlessDocument) {
|
||||
return item.id
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,51 @@
|
||||
<div class="row">
|
||||
<div class="col mb-2 mb-xl-0">
|
||||
<div class="form-inline d-flex">
|
||||
<label class="text-muted mr-2" i18n>Filter by:</label>
|
||||
<input class="form-control form-control-sm flex-grow-1" type="text" [(ngModel)]="titleFilter" placeholder="Title" i18n-placeholder>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-100 d-xl-none"></div>
|
||||
<div class="col col-xl-auto mb-2 mb-xl-0">
|
||||
<div class="d-flex">
|
||||
<app-filterable-dropdown class="mr-2 mr-md-3" title="Tags" icon="tag-fill" i18n-title
|
||||
filterPlaceholder="Filter tags" i18n-filterPlaceholder
|
||||
[items]="tags"
|
||||
[(selectionModel)]="tagSelectionModel"
|
||||
(selectionModelChange)="updateRules()"
|
||||
[multiple]="true"
|
||||
[allowSelectNone]="true"></app-filterable-dropdown>
|
||||
<app-filterable-dropdown class="mr-2 mr-md-3" title="Correspondent" icon="person-fill" i18n-title
|
||||
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
|
||||
[items]="correspondents"
|
||||
[(selectionModel)]="correspondentSelectionModel"
|
||||
(selectionModelChange)="updateRules()"
|
||||
[allowSelectNone]="true"></app-filterable-dropdown>
|
||||
<app-filterable-dropdown class="mr-2 mr-md-3" title="Document type" icon="file-earmark-fill" i18n-title
|
||||
filterPlaceholder="Filter document types" i18n-filterPlaceholder
|
||||
[items]="documentTypes"
|
||||
[(selectionModel)]="documentTypeSelectionModel"
|
||||
(selectionModelChange)="updateRules()"
|
||||
[allowSelectNone]="true"></app-filterable-dropdown>
|
||||
<app-date-dropdown class="mr-2 mr-md-3"
|
||||
title="Created" i18n-title
|
||||
(datesSet)="updateRules()"
|
||||
[(dateBefore)]="dateCreatedBefore"
|
||||
[(dateAfter)]="dateCreatedAfter"></app-date-dropdown>
|
||||
<app-date-dropdown
|
||||
[(dateBefore)]="dateAddedBefore"
|
||||
[(dateAfter)]="dateAddedAfter"
|
||||
title="Added" i18n-title
|
||||
(datesSet)="updateRules()"></app-date-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-100 d-xl-none"></div>
|
||||
<div class="col col-xl-auto mb-2 mb-xl-0">
|
||||
<button class="btn btn-link btn-sm px-0 mx-0 ml-xl-n4" [disabled]="!rulesModified" (click)="resetSelected()">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
||||
</svg> <ng-container i18n>Reset filters</ng-container>
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,213 @@
|
||||
import { Component, EventEmitter, Input, Output, OnInit, OnDestroy } 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';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
||||
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 } 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';
|
||||
|
||||
@Component({
|
||||
selector: 'app-filter-editor',
|
||||
templateUrl: './filter-editor.component.html',
|
||||
styleUrls: ['./filter-editor.component.scss']
|
||||
})
|
||||
export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
|
||||
generateFilterName() {
|
||||
if (this.filterRules.length == 1) {
|
||||
let rule = this.filterRules[0]
|
||||
switch(this.filterRules[0].rule_type) {
|
||||
|
||||
case FILTER_CORRESPONDENT:
|
||||
if (rule.value) {
|
||||
return $localize`Correspondent: ${this.correspondents.find(c => c.id == +rule.value)?.name}`
|
||||
} else {
|
||||
return $localize`Without correspondent`
|
||||
}
|
||||
|
||||
case FILTER_DOCUMENT_TYPE:
|
||||
if (rule.value) {
|
||||
return $localize`Type: ${this.documentTypes.find(dt => dt.id == +rule.value)?.name}`
|
||||
} else {
|
||||
return $localize`Without document type`
|
||||
}
|
||||
|
||||
case FILTER_HAS_TAG:
|
||||
return $localize`Tag: ${this.tags.find(t => t.id == +rule.value)?.name}`
|
||||
|
||||
case FILTER_HAS_ANY_TAG:
|
||||
if (rule.value == "false") {
|
||||
return $localize`Without any tag`
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
constructor(
|
||||
private documentTypeService: DocumentTypeService,
|
||||
private tagService: TagService,
|
||||
private correspondentService: CorrespondentService
|
||||
) { }
|
||||
|
||||
tags: PaperlessTag[] = []
|
||||
correspondents: PaperlessCorrespondent[] = []
|
||||
documentTypes: PaperlessDocumentType[] = []
|
||||
|
||||
_titleFilter = ""
|
||||
|
||||
tagSelectionModel = new FilterableDropdownSelectionModel()
|
||||
correspondentSelectionModel = new FilterableDropdownSelectionModel()
|
||||
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
|
||||
|
||||
dateCreatedBefore: string
|
||||
dateCreatedAfter: string
|
||||
dateAddedBefore: string
|
||||
dateAddedAfter: string
|
||||
|
||||
@Input()
|
||||
set filterRules (value: FilterRule[]) {
|
||||
this.documentTypeSelectionModel.clear(false)
|
||||
this.tagSelectionModel.clear(false)
|
||||
this.correspondentSelectionModel.clear(false)
|
||||
this._titleFilter = null
|
||||
this.dateAddedBefore = null
|
||||
this.dateAddedAfter = null
|
||||
this.dateCreatedBefore = null
|
||||
this.dateCreatedAfter = null
|
||||
|
||||
value.forEach(rule => {
|
||||
switch (rule.rule_type) {
|
||||
case FILTER_TITLE:
|
||||
this._titleFilter = rule.value
|
||||
break
|
||||
case FILTER_CREATED_AFTER:
|
||||
this.dateCreatedAfter = rule.value
|
||||
break
|
||||
case FILTER_CREATED_BEFORE:
|
||||
this.dateCreatedBefore = rule.value
|
||||
break
|
||||
case FILTER_ADDED_AFTER:
|
||||
this.dateAddedAfter = rule.value
|
||||
break
|
||||
case FILTER_ADDED_BEFORE:
|
||||
this.dateAddedBefore = rule.value
|
||||
break
|
||||
case FILTER_HAS_TAG:
|
||||
this.tagSelectionModel.set(rule.value ? +rule.value : null, ToggleableItemState.Selected, false)
|
||||
break
|
||||
case FILTER_HAS_ANY_TAG:
|
||||
this.tagSelectionModel.set(null, ToggleableItemState.Selected, false)
|
||||
break
|
||||
case FILTER_CORRESPONDENT:
|
||||
this.correspondentSelectionModel.set(rule.value ? +rule.value : null, ToggleableItemState.Selected, false)
|
||||
break
|
||||
case FILTER_DOCUMENT_TYPE:
|
||||
this.documentTypeSelectionModel.set(rule.value ? +rule.value : null, ToggleableItemState.Selected, false)
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
get filterRules() {
|
||||
let filterRules: FilterRule[] = []
|
||||
if (this._titleFilter) {
|
||||
filterRules.push({rule_type: FILTER_TITLE, value: this._titleFilter})
|
||||
}
|
||||
if (this.tagSelectionModel.isNoneSelected()) {
|
||||
filterRules.push({rule_type: FILTER_HAS_ANY_TAG, value: "false"})
|
||||
} else {
|
||||
this.tagSelectionModel.getSelectedItems().filter(tag => tag.id).forEach(tag => {
|
||||
filterRules.push({rule_type: FILTER_HAS_TAG, value: tag.id?.toString()})
|
||||
})
|
||||
}
|
||||
this.correspondentSelectionModel.getSelectedItems().forEach(correspondent => {
|
||||
filterRules.push({rule_type: FILTER_CORRESPONDENT, value: correspondent.id?.toString()})
|
||||
})
|
||||
this.documentTypeSelectionModel.getSelectedItems().forEach(documentType => {
|
||||
filterRules.push({rule_type: FILTER_DOCUMENT_TYPE, value: documentType.id?.toString()})
|
||||
})
|
||||
if (this.dateCreatedBefore) {
|
||||
filterRules.push({rule_type: FILTER_CREATED_BEFORE, value: this.dateCreatedBefore})
|
||||
}
|
||||
if (this.dateCreatedAfter) {
|
||||
filterRules.push({rule_type: FILTER_CREATED_AFTER, value: this.dateCreatedAfter})
|
||||
}
|
||||
if (this.dateAddedBefore) {
|
||||
filterRules.push({rule_type: FILTER_ADDED_BEFORE, value: this.dateAddedBefore})
|
||||
}
|
||||
if (this.dateAddedAfter) {
|
||||
filterRules.push({rule_type: FILTER_ADDED_AFTER, value: this.dateAddedAfter})
|
||||
}
|
||||
return filterRules
|
||||
}
|
||||
|
||||
@Output()
|
||||
filterRulesChange = new EventEmitter<FilterRule[]>()
|
||||
|
||||
@Output()
|
||||
reset = new EventEmitter()
|
||||
|
||||
@Input()
|
||||
rulesModified: boolean = false
|
||||
|
||||
updateRules() {
|
||||
this.filterRulesChange.next(this.filterRules)
|
||||
}
|
||||
|
||||
get titleFilter() {
|
||||
return this._titleFilter
|
||||
}
|
||||
|
||||
set titleFilter(value) {
|
||||
this.titleFilterDebounce.next(value)
|
||||
}
|
||||
|
||||
titleFilterDebounce: Subject<string>
|
||||
subscription: Subscription
|
||||
|
||||
ngOnInit() {
|
||||
this.tagService.listAll().subscribe(result => this.tags = result.results)
|
||||
this.correspondentService.listAll().subscribe(result => this.correspondents = result.results)
|
||||
this.documentTypeService.listAll().subscribe(result => this.documentTypes = result.results)
|
||||
|
||||
this.titleFilterDebounce = new Subject<string>()
|
||||
|
||||
this.subscription = this.titleFilterDebounce.pipe(
|
||||
debounceTime(400),
|
||||
distinctUntilChanged()
|
||||
).subscribe(title => {
|
||||
this._titleFilter = title
|
||||
this.updateRules()
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.titleFilterDebounce.complete()
|
||||
}
|
||||
|
||||
resetSelected() {
|
||||
this.reset.next()
|
||||
}
|
||||
|
||||
toggleTag(tagId: number) {
|
||||
this.tagSelectionModel.toggle(tagId)
|
||||
}
|
||||
|
||||
toggleCorrespondent(correspondentId: number) {
|
||||
this.correspondentSelectionModel.toggle(correspondentId)
|
||||
}
|
||||
|
||||
toggleDocumentType(documentTypeId: number) {
|
||||
this.documentTypeSelectionModel.toggle(documentTypeId)
|
||||
}
|
||||
|
||||
}
|
@@ -1,17 +1,17 @@
|
||||
<form [formGroup]="saveViewConfigForm" class="needs-validation" novalidate (ngSubmit)="save()">
|
||||
<form [formGroup]="saveViewConfigForm" (ngSubmit)="save()">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">Save current view</h4>
|
||||
<h4 class="modal-title" id="modal-basic-title" i18n>Save current view</h4>
|
||||
<button type="button" class="close" aria-label="Close" (click)="cancel()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<app-input-text title="Name" formControlName="name"></app-input-text>
|
||||
<app-input-check title="Show in side bar" formControlName="showInSideBar"></app-input-check>
|
||||
<app-input-check title="Show on dashboard" formControlName="showOnDashboard"></app-input-check>
|
||||
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
||||
<app-input-check i18n-title title="Show in sidebar" formControlName="showInSideBar"></app-input-check>
|
||||
<app-input-check i18n-title title="Show on dashboard" formControlName="showOnDashboard"></app-input-check>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n [disabled]="!buttonsEnabled">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" i18n [disabled]="!buttonsEnabled">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@@ -14,6 +14,12 @@ export class SaveViewConfigDialogComponent implements OnInit {
|
||||
@Output()
|
||||
public saveClicked = new EventEmitter()
|
||||
|
||||
@Input()
|
||||
error
|
||||
|
||||
@Input()
|
||||
buttonsEnabled = true
|
||||
|
||||
_defaultName = ""
|
||||
|
||||
get defaultName() {
|
||||
@@ -26,7 +32,6 @@ export class SaveViewConfigDialogComponent implements OnInit {
|
||||
this.saveViewConfigForm.patchValue({name: value})
|
||||
}
|
||||
|
||||
|
||||
saveViewConfigForm = new FormGroup({
|
||||
name: new FormControl(''),
|
||||
showInSideBar: new FormControl(false),
|
||||
|
@@ -1,43 +0,0 @@
|
||||
<div class="btn-group" ngbDropdown role="group">
|
||||
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="dateBefore || dateAfter ? 'btn-primary' : 'btn-outline-primary'">
|
||||
{{title}}
|
||||
</button>
|
||||
<div class="dropdown-menu date-filter shadow pt-0" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
|
||||
<div class="list-group list-group-flush">
|
||||
<button class="list-group-item small list-goup list-group-item-action d-flex p-2 pl-3" (click)="clear()">Clear</button>
|
||||
<button *ngFor="let range of [7, 30, 'month', 'year']" class="list-group-item small list-goup list-group-item-action d-flex p-2 pl-3" role="menuitem" (click)="setDateQuickFilter(range)">
|
||||
<ng-container *ngIf="isStringRange(range)">This </ng-container>
|
||||
{{ range }}
|
||||
<ng-container *ngIf="!isStringRange(range)"> days</ng-container>
|
||||
</button>
|
||||
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
|
||||
<div>Before</div>
|
||||
<div class="input-group input-group-sm">
|
||||
<input class="form-control" type="text" placeholder="yyyy-mm-dd" name="before" [(ngModel)]="_dateBefore" [maxDate]="this._maxDate" ngbDatepicker (dateSelect)="onBeforeSelected($event)" #dpBefore="ngbDatepicker">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary btn-sm" (click)="dpBefore.toggle()" type="button">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-calendar-date" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" 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"/>
|
||||
<path d="M6.445 11.688V6.354h-.633A12.6 12.6 0 0 0 4.5 7.16v.695c.375-.257.969-.62 1.258-.777h.012v4.61h.675zm1.188-1.305c.047.64.594 1.406 1.703 1.406 1.258 0 2-1.066 2-2.871 0-1.934-.781-2.668-1.953-2.668-.926 0-1.797.672-1.797 1.809 0 1.16.824 1.77 1.676 1.77.746 0 1.23-.376 1.383-.79h.027c-.004 1.316-.461 2.164-1.305 2.164-.664 0-1.008-.45-1.05-.82h-.684zm2.953-2.317c0 .696-.559 1.18-1.184 1.18-.601 0-1.144-.383-1.144-1.2 0-.823.582-1.21 1.168-1.21.633 0 1.16.398 1.16 1.23z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
|
||||
<div>After</div>
|
||||
<div class="input-group input-group-sm">
|
||||
<input class="form-control form-control-sm" type="text" placeholder="yyyy-mm-dd" name="after" [(ngModel)]="_dateAfter" [maxDate]="this._maxDate" ngbDatepicker (dateSelect)="onAfterSelected($event)" #dpAfter="ngbDatepicker">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary btn-sm" (click)="dpAfter.toggle()" type="button">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-calendar-date" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" 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"/>
|
||||
<path d="M6.445 11.688V6.354h-.633A12.6 12.6 0 0 0 4.5 7.16v.695c.375-.257.969-.62 1.258-.777h.012v4.61h.675zm1.188-1.305c.047.64.594 1.406 1.703 1.406 1.258 0 2-1.066 2-2.871 0-1.934-.781-2.668-1.953-2.668-.926 0-1.797.672-1.797 1.809 0 1.16.824 1.77 1.676 1.77.746 0 1.23-.376 1.383-.79h.027c-.004 1.316-.461 2.164-1.305 2.164-.664 0-1.008-.45-1.05-.82h-.684zm2.953-2.317c0 .696-.559 1.18-1.184 1.18-.601 0-1.144-.383-1.144-1.2 0-.823.582-1.21 1.168-1.21.633 0 1.16.398 1.16 1.23z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -1,112 +0,0 @@
|
||||
import { Component, EventEmitter, Input, Output, ElementRef, ViewChild, SimpleChange } from '@angular/core';
|
||||
import { NgbDate, NgbDateStruct, NgbDatepicker } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
export interface DateSelection {
|
||||
before?: NgbDateStruct
|
||||
after?: NgbDateStruct
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-filter-dropdown-date',
|
||||
templateUrl: './filter-dropdown-date.component.html',
|
||||
styleUrls: ['./filter-dropdown-date.component.scss']
|
||||
})
|
||||
export class FilterDropdownDateComponent {
|
||||
|
||||
@Input()
|
||||
dateBefore: NgbDateStruct
|
||||
|
||||
@Input()
|
||||
dateAfter: NgbDateStruct
|
||||
|
||||
@Input()
|
||||
title: string
|
||||
|
||||
@Output()
|
||||
datesSet = new EventEmitter<DateSelection>()
|
||||
|
||||
@ViewChild('dpAfter') dpAfter: NgbDatepicker
|
||||
@ViewChild('dpBefore') dpBefore: NgbDatepicker
|
||||
|
||||
_dateBefore: NgbDateStruct
|
||||
_dateAfter: NgbDateStruct
|
||||
|
||||
get _maxDate(): NgbDate {
|
||||
let date = new Date()
|
||||
return NgbDate.from({year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate()})
|
||||
}
|
||||
|
||||
isStringRange(range: any) {
|
||||
return typeof range == 'string'
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChange) {
|
||||
// this is a hacky workaround perhaps because of https://github.com/angular/angular/issues/11097
|
||||
let dateString: string = ''
|
||||
let dateAfterChange: SimpleChange
|
||||
let dateBeforeChange: SimpleChange
|
||||
if (changes) {
|
||||
dateAfterChange = changes['dateAfter']
|
||||
dateBeforeChange = changes['dateBefore']
|
||||
}
|
||||
|
||||
if (this.dpBefore && this.dpAfter) {
|
||||
let dpAfterElRef: ElementRef = this.dpAfter['_elRef']
|
||||
let dpBeforeElRef: ElementRef = this.dpBefore['_elRef']
|
||||
|
||||
if (dateAfterChange && dateAfterChange.currentValue) {
|
||||
let dateAfterDate = dateAfterChange.currentValue as NgbDateStruct
|
||||
dateString = `${dateAfterDate.year}-${dateAfterDate.month.toString().padStart(2,'0')}-${dateAfterDate.day.toString().padStart(2,'0')}`
|
||||
dpAfterElRef.nativeElement.value = dateString
|
||||
} else if (dateBeforeChange && dateBeforeChange.currentValue) {
|
||||
let dateBeforeDate = dateBeforeChange.currentValue as NgbDateStruct
|
||||
dateString = `${dateBeforeDate.year}-${dateBeforeDate.month.toString().padStart(2,'0')}-${dateBeforeDate.day.toString().padStart(2,'0')}`
|
||||
dpBeforeElRef.nativeElement.value = dateString
|
||||
} else {
|
||||
dpAfterElRef.nativeElement.value = dateString
|
||||
dpBeforeElRef.nativeElement.value = dateString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setDateQuickFilter(range: any) {
|
||||
let date = new Date()
|
||||
let newDate: NgbDateStruct = { year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate() }
|
||||
switch (typeof range) {
|
||||
case 'number':
|
||||
date.setDate(date.getDate() - range)
|
||||
newDate.year = date.getFullYear()
|
||||
newDate.month = date.getMonth() + 1
|
||||
newDate.day = date.getDate()
|
||||
break
|
||||
|
||||
case 'string':
|
||||
newDate.day = 1
|
||||
if (range == 'year') newDate.month = 1
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
this._dateAfter = newDate
|
||||
this._dateBefore = null
|
||||
this.datesSet.emit({after: newDate, before: null})
|
||||
}
|
||||
|
||||
onBeforeSelected(date: NgbDateStruct) {
|
||||
this._dateBefore = date
|
||||
this.datesSet.emit({after: this._dateAfter, before: date})
|
||||
}
|
||||
|
||||
onAfterSelected(date: NgbDateStruct) {
|
||||
this._dateAfter = date
|
||||
this.datesSet.emit({after: date, before: this._dateBefore})
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._dateBefore = null
|
||||
this._dateAfter = null
|
||||
this.datesSet.emit({after: null, before: null})
|
||||
}
|
||||
}
|
@@ -1,12 +0,0 @@
|
||||
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-left-0 border-right-0 border-bottom" role="menuitem" (click)="toggleItem()">
|
||||
<div class="selected-icon mr-1">
|
||||
<svg *ngIf="selected" width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-check" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M10.97 4.97a.75.75 0 0 1 1.071 1.05l-3.992 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.236.236 0 0 1 .02-.022z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mr-1">
|
||||
<app-tag *ngIf="isTag; else displayName" [tag]="item" [clickable]="true" linkTitle="Filter by tag"></app-tag>
|
||||
<ng-template #displayName><small>{{item.name}}</small></ng-template>
|
||||
</div>
|
||||
<div class="badge badge-light rounded-pill ml-auto mr-1">{{item.document_count}}</div>
|
||||
</button>
|
@@ -1,25 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FilterDropodownButtonComponent } from './filter-dropdown-button.component';
|
||||
|
||||
describe('FilterDropodownButtonComponent', () => {
|
||||
let component: FilterDropodownButtonComponent;
|
||||
let fixture: ComponentFixture<FilterDropodownButtonComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ FilterDropodownButtonComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FilterDropodownButtonComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -1,32 +0,0 @@
|
||||
import { Component, EventEmitter, Input, Output, OnInit } 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';
|
||||
|
||||
@Component({
|
||||
selector: 'app-filter-dropdown-button',
|
||||
templateUrl: './filter-dropdown-button.component.html',
|
||||
styleUrls: ['./filter-dropdown-button.component.scss']
|
||||
})
|
||||
export class FilterDropdownButtonComponent implements OnInit {
|
||||
|
||||
@Input()
|
||||
item: PaperlessTag | PaperlessDocumentType | PaperlessCorrespondent
|
||||
|
||||
@Input()
|
||||
selected: boolean
|
||||
|
||||
@Output()
|
||||
toggle = new EventEmitter()
|
||||
|
||||
isTag: boolean
|
||||
|
||||
ngOnInit() {
|
||||
this.isTag = 'is_inbox_tag' in this.item // ~ this.item instanceof PaperlessTag
|
||||
}
|
||||
|
||||
toggleItem(): void {
|
||||
this.selected = !this.selected
|
||||
this.toggle.emit(this.item)
|
||||
}
|
||||
}
|
@@ -1,29 +0,0 @@
|
||||
<div class="btn-group" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #filterDropdown="ngbDropdown">
|
||||
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="itemsSelected?.length > 0 ? 'btn-primary' : 'btn-outline-primary'">
|
||||
<div class="d-none d-md-inline">{{title}}</div>
|
||||
<div class="d-inline-block d-md-none">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
|
||||
</svg>
|
||||
</div>
|
||||
<ng-container *ngIf="itemsSelected?.length > 0">
|
||||
<div class="badge bg-secondary text-light rounded-pill badge-corner">
|
||||
{{itemsSelected?.length}}
|
||||
</div>
|
||||
</ng-container>
|
||||
</button>
|
||||
<div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
|
||||
<div class="list-group list-group-flush">
|
||||
<div class="list-group-item">
|
||||
<div class="input-group input-group-sm">
|
||||
<input class="form-control" type="text" [(ngModel)]="filterText" placeholder="Filter {{title}}" (keyup.enter)="listFilterEnter()" #listFilterTextInput>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="items" class="items">
|
||||
<ng-container *ngFor="let item of items | filter: filterText; let i = index">
|
||||
<app-filter-dropdown-button [item]="item" [selected]="isItemSelected(item)" (toggle)="toggleItem($event)"></app-filter-dropdown-button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user