mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-08-10 00:18:57 +00:00
Compare commits
377 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
34a06435cf | ||
![]() |
fad6e7284a | ||
![]() |
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 | ||
![]() |
406f8daa12 | ||
![]() |
0a4a06b991 | ||
![]() |
41e842a9e6 | ||
![]() |
1fb3316436 | ||
![]() |
2f26d07480 | ||
![]() |
360fc081bb | ||
![]() |
4cae338479 | ||
![]() |
48d83e166b | ||
![]() |
29e9d7d793 | ||
![]() |
dcbc0ea2e5 | ||
![]() |
0e93f7eba5 | ||
![]() |
d9e3895f34 | ||
![]() |
f06e2c1089 | ||
![]() |
fa7b90a584 | ||
![]() |
37c2051e01 | ||
![]() |
3561935e1b | ||
![]() |
7669679fb1 | ||
![]() |
2e44c18cdb | ||
![]() |
86da578774 | ||
![]() |
46f778111c | ||
![]() |
7dfcc7f47b | ||
![]() |
39b35c090b | ||
![]() |
ee4e026ba2 | ||
![]() |
de6ba3489a | ||
![]() |
6381093386 | ||
![]() |
fd6bfd02ce | ||
![]() |
1da652ba4d | ||
![]() |
9a4190bedf | ||
![]() |
561db8607a | ||
![]() |
d91fa99e77 | ||
![]() |
400da7bbc5 | ||
![]() |
1041504cb1 | ||
![]() |
26b40e06f8 | ||
![]() |
df8235de13 | ||
![]() |
12e45624db | ||
![]() |
da3695e3a4 | ||
![]() |
24c53e78a7 | ||
![]() |
275bd96ba8 | ||
![]() |
01d448ecde | ||
![]() |
55a6dca373 | ||
![]() |
fbb2da42dc | ||
![]() |
fb9d750684 | ||
![]() |
b8469946a8 | ||
![]() |
03f071fd27 | ||
![]() |
34c42c4339 | ||
![]() |
b45bd66573 | ||
![]() |
3b2bc292d8 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -85,3 +85,4 @@ scripts/nuke
|
|||||||
|
|
||||||
# this is where the compiled frontend is moved to.
|
# this is where the compiled frontend is moved to.
|
||||||
/src/documents/static/frontend/
|
/src/documents/static/frontend/
|
||||||
|
/docs/.vscode/settings.json
|
||||||
|
3
Pipfile
3
Pipfile
@@ -40,8 +40,9 @@ whitenoise = "~=5.2.0"
|
|||||||
watchdog = "*"
|
watchdog = "*"
|
||||||
whoosh="~=2.7.4"
|
whoosh="~=2.7.4"
|
||||||
inotifyrecursive = "~=0.3.4"
|
inotifyrecursive = "~=0.3.4"
|
||||||
ocrmypdf = "*"
|
ocrmypdf = "~=11.4.5"
|
||||||
tqdm = "*"
|
tqdm = "*"
|
||||||
|
tika = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
coveralls = "*"
|
coveralls = "*"
|
||||||
|
629
Pipfile.lock
generated
629
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "3d576f289958226a7583e4c471c7f8c11bff6933bf093185f623cfb381a92412"
|
"sha256": "c35d84fd7f4f1c7d599039712362935e7c41a226b0ab3d83d8c1c2fb2ad0962a"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -44,6 +44,13 @@
|
|||||||
],
|
],
|
||||||
"version": "==1.17.12"
|
"version": "==1.17.12"
|
||||||
},
|
},
|
||||||
|
"certifi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
|
||||||
|
"sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
|
||||||
|
],
|
||||||
|
"version": "==2020.12.5"
|
||||||
|
},
|
||||||
"cffi": {
|
"cffi": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e",
|
"sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e",
|
||||||
@@ -89,50 +96,40 @@
|
|||||||
},
|
},
|
||||||
"chardet": {
|
"chardet": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
|
||||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.1'",
|
"markers": "python_version >= '3.1'",
|
||||||
"version": "==3.0.4"
|
"version": "==4.0.0"
|
||||||
},
|
},
|
||||||
"coloredlogs": {
|
"coloredlogs": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:346f58aad6afd48444c2468618623638dadab76e4e70d5e10822676f2d32226a",
|
"sha256:5e78691e2673a8e294499e1832bb13efcfb44a86b92e18109fa18951093218ab",
|
||||||
"sha256:a1fab193d2053aa6c0a97608c4342d031f1f93a3d1218432c59322441d31a505",
|
"sha256:b7f630a8297a66984b6bae0f6a1b0e0afb9f2f6838ea3bfa58f50d3d13e133d6"
|
||||||
"sha256:b0c2124367d4f72bd739f48e1f61491b4baf145d6bda33b606b4a53cb3f96a97"
|
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
"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": {
|
"cryptography": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:07ca431b788249af92764e3be9a488aa1d39a0bc3be313d826bbec690417e538",
|
"sha256:0003a52a123602e1acee177dc90dd201f9bb1e73f24a070db7d36c588e8f5c7d",
|
||||||
"sha256:13b88a0bd044b4eae1ef40e265d006e34dbcde0c2f1e15eb9896501b2d8f6c6f",
|
"sha256:0e85aaae861d0485eb5a79d33226dd6248d2a9f133b81532c8f5aae37de10ff7",
|
||||||
"sha256:257dab4f368fae15f378ea9a4d2799bf3696668062de0e9fa0ebb7a738a6917d",
|
"sha256:594a1db4511bc4d960571536abe21b4e5c3003e8750ab8365fafce71c5d86901",
|
||||||
"sha256:32434673d8505b42c0de4de86da8c1620651abd24afe91ae0335597683ed1b77",
|
"sha256:69e836c9e5ff4373ce6d3ab311c1a2eed274793083858d3cd4c7d12ce20d5f9c",
|
||||||
"sha256:3cd75a683b15576cfc822c7c5742b3276e50b21a06672dc3a800a2d5da4ecd1b",
|
"sha256:788a3c9942df5e4371c199d10383f44a105d67d401fb4304178020142f020244",
|
||||||
"sha256:4e7268a0ca14536fecfdf2b00297d4e407da904718658c1ff1961c713f90fd33",
|
"sha256:7e177e4bea2de937a584b13645cab32f25e3d96fc0bc4a4cf99c27dc77682be6",
|
||||||
"sha256:545a8550782dda68f8cdc75a6e3bf252017aa8f75f19f5a9ca940772fc0cb56e",
|
"sha256:83d9d2dfec70364a74f4e7c70ad04d3ca2e6a08b703606993407bf46b97868c5",
|
||||||
"sha256:55d0b896631412b6f0c7de56e12eb3e261ac347fbaa5d5e705291a9016e5f8cb",
|
"sha256:84ef7a0c10c24a7773163f917f1cb6b4444597efd505a8aed0a22e8c4780f27e",
|
||||||
"sha256:5849d59358547bf789ee7e0d7a9036b2d29e9a4ddf1ce5e06bb45634f995c53e",
|
"sha256:982f661bffc7a24b6d4f8ebe3291f17cf3833a0941c6f4d9d55c790b9aa2cdb3",
|
||||||
"sha256:59f7d4cfea9ef12eb9b14b83d79b432162a0a24a91ddc15c2c9bf76a68d96f2b",
|
"sha256:9e21301f7a1e7c03dbea73e8602905a4ebba641547a462b26dd03451e5769e7c",
|
||||||
"sha256:6dc59630ecce8c1f558277ceb212c751d6730bd12c80ea96b4ac65637c4f55e7",
|
"sha256:9f6b0492d111b43de5f70052e24c1f0951cb9e6022188ebcb1cc3a3d301469b0",
|
||||||
"sha256:7117319b44ed1842c617d0a452383a5a052ec6aa726dfbaffa8b94c910444297",
|
"sha256:a69bd3c68b98298f490e84519b954335154917eaab52cf582fa2c5c7efc6e812",
|
||||||
"sha256:75e8e6684cf0034f6bf2a97095cb95f81537b12b36a8fedf06e73050bb171c2d",
|
"sha256:b4890d5fb9b7a23e3bf8abf5a8a7da8e228f1e97dc96b30b95685df840b6914a",
|
||||||
"sha256:7b8d9d8d3a9bd240f453342981f765346c87ade811519f98664519696f8e6ab7",
|
"sha256:c366df0401d1ec4e548bebe8f91d55ebcc0ec3137900d214dd7aac8427ef3030",
|
||||||
"sha256:a035a10686532b0587d58a606004aa20ad895c60c4d029afa245802347fab57b",
|
"sha256:dc42f645f8f3a489c3dd416730a514e7a91a59510ddaadc09d04224c098d3302"
|
||||||
"sha256:a4e27ed0b2504195f855b52052eadcc9795c59909c9d84314c5408687f933fc7",
|
|
||||||
"sha256:a733671100cd26d816eed39507e585c156e4498293a907029969234e5e634bc4",
|
|
||||||
"sha256:a75f306a16d9f9afebfbedc41c8c2351d8e61e818ba6b4c40815e2b5740bb6b8",
|
|
||||||
"sha256:bd717aa029217b8ef94a7d21632a3bb5a4e7218a4513d2521c2a2fd63011e98b",
|
|
||||||
"sha256:d25cecbac20713a7c3bc544372d42d8eafa89799f492a43b79e1dfd650484851",
|
|
||||||
"sha256:d26a2557d8f9122f9bf445fc7034242f4375bd4e95ecda007667540270965b13",
|
|
||||||
"sha256:d3545829ab42a66b84a9aaabf216a4dce7f16dbc76eb69be5c302ed6b8f4a29b",
|
|
||||||
"sha256:d3d5e10be0cf2a12214ddee45c6bd203dab435e3d83b4560c03066eda600bfe3",
|
|
||||||
"sha256:efe15aca4f64f3a7ea0c09c87826490e50ed166ce67368a68f315ea0807a20df"
|
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||||
"version": "==3.2.1"
|
"version": "==3.3.1"
|
||||||
},
|
},
|
||||||
"dateparser": {
|
"dateparser": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -144,19 +141,19 @@
|
|||||||
},
|
},
|
||||||
"django": {
|
"django": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:5c866205f15e7a7123f1eec6ab939d22d5bde1416635cab259684af66d8e48a2",
|
"sha256:2d78425ba74c7a1a74b196058b261b9733a8570782f4e2828974777ccca7edf7",
|
||||||
"sha256:edb10b5c45e7e9c0fb1dc00b76ec7449aca258a39ffd613dbd078c51d19c9f03"
|
"sha256:efa2ab96b33b20c2182db93147a0c3cd7769d418926f9e9f140a60dca7c64ca9"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.1.4"
|
"version": "==3.1.5"
|
||||||
},
|
},
|
||||||
"django-cors-headers": {
|
"django-cors-headers": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:9322255c296d5f75089571f29e520c83ff9693df17aa3cf9f6a4bea7c6740169",
|
"sha256:5665fc1b1aabf1b678885cf6f8f8bd7da36ef0a978375e767d491b48d3055d8f",
|
||||||
"sha256:db82b2840f667d47872ae3e4a4e0a0d72fbecb42779b8aa233fa8bb965f7836a"
|
"sha256:ba898dd478cd4be3a38ebc3d8729fa4d044679f8c91b2684edee41129d7e968a"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.5.0"
|
"version": "==3.6.0"
|
||||||
},
|
},
|
||||||
"django-extensions": {
|
"django-extensions": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -192,7 +189,8 @@
|
|||||||
},
|
},
|
||||||
"djangorestframework": {
|
"djangorestframework": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0209bafcb7b5010fdfec784034f059d512256424de2a0f084cb82b096d6dd6a7"
|
"sha256:0209bafcb7b5010fdfec784034f059d512256424de2a0f084cb82b096d6dd6a7",
|
||||||
|
"sha256:0898182b4737a7b584a2c73735d89816343369f259fea932d90dc78e35d8ac33"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.12.2"
|
"version": "==3.12.2"
|
||||||
@@ -223,19 +221,28 @@
|
|||||||
},
|
},
|
||||||
"humanfriendly": {
|
"humanfriendly": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:175ffa628aa76da2c17369a5da5856084562cc66dfe7f82ae93ca3ef175277a6",
|
"sha256:066562956639ab21ff2676d1fda0b5987e985c534fc76700a19bd54bcb81121d",
|
||||||
"sha256:3c9ab8d28e88e6cc998e41963357736dafd555ee5bb666b50e42f6ce28dd3e3d"
|
"sha256:d5c731705114b9ad673754f3317d9fa4c23212f36b29bdc4272a892eafc9bc72"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
"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": {
|
"imap-tools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:72bf46dc135b039a5d5b59f4e079242ac15eac02a30038e8cb2dec7b153cab65",
|
"sha256:7d2d25b35117a3750c3b561dd93cc2fcb24cdc457830a049796c639f4371e317",
|
||||||
"sha256:75dc1c72dd76d9e577df26a1e0ec3a809b5eebce77678851458dcd2eae127ac9"
|
"sha256:80088839cd1959f20c44206cdad4463ca1e7647ff67cf5b0e31e810fb6aaa6c4"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.33.0"
|
"version": "==0.34.0"
|
||||||
},
|
},
|
||||||
"img2pdf": {
|
"img2pdf": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -246,11 +253,11 @@
|
|||||||
},
|
},
|
||||||
"importlib-metadata": {
|
"importlib-metadata": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:6112e21359ef8f344e7178aa5b72dc6e62b38b0d008e6d3cb212c5b84df72013",
|
"sha256:5c5a2720817414a6c41f0a49993908068243ae02c1635a228126519b509c8aed",
|
||||||
"sha256:b0c2d3b226157ae4517d9625decf63591461c66b3a808c2666d538946519d170"
|
"sha256:bf792d480abbd5eda85794e4afb09dd538393f7d6e6ffef6e9f03d2014cf9450"
|
||||||
],
|
],
|
||||||
"markers": "python_version < '3.8'",
|
"markers": "python_version < '3.8'",
|
||||||
"version": "==3.1.1"
|
"version": "==3.3.0"
|
||||||
},
|
},
|
||||||
"inotify-simple": {
|
"inotify-simple": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -270,11 +277,11 @@
|
|||||||
},
|
},
|
||||||
"joblib": {
|
"joblib": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:698c311779f347cf6b7e6b8a39bb682277b8ee4aba8cf9507bc0cf4cd4737b72",
|
"sha256:75ead23f13484a2a414874779d69ade40d4fa1abe62b222a23cd50d4bc822f6f",
|
||||||
"sha256:9e284edd6be6b71883a63c9b7f124738a3c16195513ad940eae7e3438de885d5"
|
"sha256:7ad866067ac1fdec27d51c8678ea760601b70e32ff1881d4dc8e1171f2b64b24"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==0.17.0"
|
"version": "==1.0.0"
|
||||||
},
|
},
|
||||||
"langdetect": {
|
"langdetect": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -332,67 +339,60 @@
|
|||||||
},
|
},
|
||||||
"numpy": {
|
"numpy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:08308c38e44cc926bdfce99498b21eec1f848d24c302519e64203a8da99a97db",
|
"sha256:012426a41bc9ab63bb158635aecccc7610e3eff5d31d1eb43bc099debc979d94",
|
||||||
"sha256:09c12096d843b90eafd01ea1b3307e78ddd47a55855ad402b157b6c4862197ce",
|
"sha256:06fab248a088e439402141ea04f0fffb203723148f6ee791e9c75b3e9e82f080",
|
||||||
"sha256:13d166f77d6dc02c0a73c1101dd87fdf01339febec1030bd810dcd53fff3b0f1",
|
"sha256:0eef32ca3132a48e43f6a0f5a82cb508f22ce5a3d6f67a8329c81c8e226d3f6e",
|
||||||
"sha256:141ec3a3300ab89c7f2b0775289954d193cc8edb621ea05f99db9cb181530512",
|
"sha256:1ded4fce9cfaaf24e7a0ab51b7a87be9038ea1ace7f34b841fe3b6894c721d1c",
|
||||||
"sha256:16c1b388cc31a9baa06d91a19366fb99ddbe1c7b205293ed072211ee5bac1ed2",
|
"sha256:2e55195bc1c6b705bfd8ad6f288b38b11b1af32f3c8289d6c50d47f950c12e76",
|
||||||
"sha256:18bed2bcb39e3f758296584337966e68d2d5ba6aab7e038688ad53c8f889f757",
|
"sha256:2ea52bd92ab9f768cc64a4c3ef8f4b2580a17af0a5436f6126b08efbd1838371",
|
||||||
"sha256:1aeef46a13e51931c0b1cf8ae1168b4a55ecd282e6688fdb0a948cc5a1d5afb9",
|
"sha256:36674959eed6957e61f11c912f71e78857a8d0604171dfd9ce9ad5cbf41c511c",
|
||||||
"sha256:27d3f3b9e3406579a8af3a9f262f5339005dd25e0ecf3cf1559ff8a49ed5cbf2",
|
"sha256:384ec0463d1c2671170901994aeb6dce126de0a95ccc3976c43b0038a37329c2",
|
||||||
"sha256:2a2740aa9733d2e5b2dfb33639d98a64c3b0f24765fed86b0fd2aec07f6a0a08",
|
"sha256:39b70c19ec771805081578cc936bbe95336798b7edf4732ed102e7a43ec5c07a",
|
||||||
"sha256:4377e10b874e653fe96985c05feed2225c912e328c8a26541f7fc600fb9c637b",
|
"sha256:400580cbd3cff6ffa6293df2278c75aef2d58d8d93d3c5614cd67981dae68ceb",
|
||||||
"sha256:448ebb1b3bf64c0267d6b09a7cba26b5ae61b6d2dbabff7c91b660c7eccf2bdb",
|
"sha256:43d4c81d5ffdff6bae58d66a3cd7f54a7acd9a0e7b18d97abb255defc09e3140",
|
||||||
"sha256:50e86c076611212ca62e5a59f518edafe0c0730f7d9195fec718da1a5c2bb1fc",
|
"sha256:50a4a0ad0111cc1b71fa32dedd05fa239f7fb5a43a40663269bb5dc7877cfd28",
|
||||||
"sha256:5734bdc0342aba9dfc6f04920988140fb41234db42381cf7ccba64169f9fe7ac",
|
"sha256:603aa0706be710eea8884af807b1b3bc9fb2e49b9f4da439e76000f3b3c6ff0f",
|
||||||
"sha256:5ddd1dfa2be066595c1993165b4cae84b9866b12339d0c903db7f21a094324a3",
|
"sha256:6149a185cece5ee78d1d196938b2a8f9d09f5a5ebfbba66969302a778d5ddd1d",
|
||||||
"sha256:64324f64f90a9e4ef732be0928be853eee378fd6a01be21a0a8469c4f2682c83",
|
"sha256:6373751c4b6fd325606d29dd98dc2bf7092485ad20aafbfc6a177acd3b89059e",
|
||||||
"sha256:6ae6c680f3ebf1cf7ad1d7748868b39d9f900836df774c453c11c5440bc15b36",
|
"sha256:759e4095edc3c1b3ac031f34d9459fa781777a93ccc633a472a5468587a190ff",
|
||||||
"sha256:6d7593a705d662be5bfe24111af14763016765f43cb6923ed86223f965f52387",
|
"sha256:7fb43004bce0ca31d8f13a6eb5e943fa73371381e53f7074ed21a4cb786c32f8",
|
||||||
"sha256:8cac8790a6b1ddf88640a9267ee67b1aee7a57dfa2d2dd33999d080bc8ee3a0f",
|
"sha256:811daee36a58dc79cf3d8bdd4a490e4277d0e4b7d103a001a4e73ddb48e7e6aa",
|
||||||
"sha256:8ece138c3a16db8c1ad38f52eb32be6086cc72f403150a79336eb2045723a1ad",
|
"sha256:8b5e972b43c8fc27d56550b4120fe6257fdc15f9301914380b27f74856299fea",
|
||||||
"sha256:9eeb7d1d04b117ac0d38719915ae169aa6b61fca227b0b7d198d43728f0c879c",
|
"sha256:99abf4f353c3d1a0c7a5f27699482c987cf663b1eac20db59b8c7b061eabd7fc",
|
||||||
"sha256:a09f98011236a419ee3f49cedc9ef27d7a1651df07810ae430a6b06576e0b414",
|
"sha256:a0d53e51a6cb6f0d9082decb7a4cb6dfb33055308c4c44f53103c073f649af73",
|
||||||
"sha256:a5d897c14513590a85774180be713f692df6fa8ecf6483e561a6d47309566f37",
|
"sha256:a12ff4c8ddfee61f90a1633a4c4afd3f7bcb32b11c52026c92a12e1325922d0d",
|
||||||
"sha256:ad6f2ff5b1989a4899bf89800a671d71b1612e5ff40866d1f4d8bcf48d4e5764",
|
"sha256:a4646724fba402aa7504cd48b4b50e783296b5e10a524c7a6da62e4a8ac9698d",
|
||||||
"sha256:c42c4b73121caf0ed6cd795512c9c09c52a7287b04d105d112068c1736d7c753",
|
"sha256:a76f502430dd98d7546e1ea2250a7360c065a5fdea52b2dffe8ae7180909b6f4",
|
||||||
"sha256:cb1017eec5257e9ac6209ac172058c430e834d5d2bc21961dceeb79d111e5909",
|
"sha256:a9d17f2be3b427fbb2bce61e596cf555d6f8a56c222bd2ca148baeeb5e5c783c",
|
||||||
"sha256:d6c7bb82883680e168b55b49c70af29b84b84abb161cbac2800e8fcb6f2109b6",
|
"sha256:ab83f24d5c52d60dbc8cd0528759532736b56db58adaa7b5f1f76ad551416a1e",
|
||||||
"sha256:e452dc66e08a4ce642a961f134814258a082832c78c90351b75c41ad16f79f63",
|
"sha256:aeb9ed923be74e659984e321f609b9ba54a48354bfd168d21a2b072ed1e833ea",
|
||||||
"sha256:e5b6ed0f0b42317050c88022349d994fe72bfe35f5908617512cd8c8ef9da2a9",
|
"sha256:c843b3f50d1ab7361ca4f0b3639bf691569493a56808a0b0c54a051d260b7dbd",
|
||||||
"sha256:e9b30d4bd69498fc0c3fe9db5f62fffbb06b8eb9321f92cc970f2969be5e3949",
|
"sha256:cae865b1cae1ec2663d8ea56ef6ff185bad091a5e33ebbadd98de2cfa3fa668f",
|
||||||
"sha256:ec149b90019852266fec2341ce1db513b843e496d5a8e8cdb5ced1923a92faab",
|
"sha256:cc6bd4fd593cb261332568485e20a0712883cf631f6f5e8e86a52caa8b2b50ff",
|
||||||
"sha256:edb01671b3caae1ca00881686003d16c2209e07b7ef8b7639f1867852b948f7c",
|
"sha256:cf2402002d3d9f91c8b01e66fbb436a4ed01c6498fffed0e4c7566da1d40ee1e",
|
||||||
"sha256:f0d3929fe88ee1c155129ecd82f981b8856c5d97bcb0d5f23e9b4242e79d1de3",
|
"sha256:d051ec1c64b85ecc69531e1137bb9751c6830772ee5c1c426dbcfe98ef5788d7",
|
||||||
"sha256:f29454410db6ef8126c83bd3c968d143304633d45dc57b51252afbd79d700893",
|
"sha256:d6631f2e867676b13026e2846180e2c13c1e11289d67da08d71cacb2cd93d4aa",
|
||||||
"sha256:fe45becb4c2f72a0907c1d0246ea6449fe7a9e2293bb0e11c4e9a32bb0930a15",
|
"sha256:dbd18bcf4889b720ba13a27ec2f2aac1981bd41203b3a3b27ba7a33f88ae4827",
|
||||||
"sha256:fedbd128668ead37f33917820b704784aff695e0019309ad446a6d0b065b57e4"
|
"sha256:df609c82f18c5b9f6cb97271f03315ff0dbe481a2a02e56aeb1b1a985ce38e60"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==1.19.4"
|
"version": "==1.19.5"
|
||||||
},
|
},
|
||||||
"ocrmypdf": {
|
"ocrmypdf": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:91e7394172cedb3be801a229dbd3d308fb5ae80cbc3a77879fa7954beea407b1",
|
"sha256:416a9c4321bfc844f250694b8c68ebb538f60609bbc8686bd9f84a13c5127d68",
|
||||||
"sha256:e550b8e884150accab7ea41f4a576b5844594cb5cbd6ed514fbf1206720343ad"
|
"sha256:f45fc7e844e6026d6080a623a2936be120fc077d99aaa599df022acf35fb31e6"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==11.3.4"
|
"version": "==11.4.5"
|
||||||
},
|
|
||||||
"pathtools": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0",
|
|
||||||
"sha256:d77d982475e87f32b82157a43b09f0a5ef3e66c1d8f3c7eb8d2580e783cd8202"
|
|
||||||
],
|
|
||||||
"version": "==0.1.2"
|
|
||||||
},
|
},
|
||||||
"pathvalidate": {
|
"pathvalidate": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1697c8ea71ff4c48e7aa0eda72fe4581404be8f41e51a17363ef682dd6824d35",
|
"sha256:378c8b319838a255c00ab37f664686b75f0aabea4444d6c5a34effbec6738285",
|
||||||
"sha256:32d30dbacb711c16bb188b12ce7e9a46b41785f50a12f64500f747480a4b6ee3"
|
"sha256:cae8ad5cd9223c5c1f4bc4e2ef0cd4c5e89acd2d698fdb7610ee108b9be654d2"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.3.0"
|
"version": "==2.3.2"
|
||||||
},
|
},
|
||||||
"pdfminer.six": {
|
"pdfminer.six": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -411,65 +411,65 @@
|
|||||||
},
|
},
|
||||||
"pikepdf": {
|
"pikepdf": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0829bd5dacd73bb4a37e7575bae523f49603479755563c92ddb55c206700cab1",
|
"sha256:0e67e5beeeed5422b3b8e862e4777fed5a4cd3c72e711e2a449a65d9ee641448",
|
||||||
"sha256:0d2b631077cd6af6e4d1b396208020705842610a6f13fab489d5f9c47916baa2",
|
"sha256:138155ae1f71634cd6eca79f5517f77b2067ef0bd5b627ea9414e308fe868dc5",
|
||||||
"sha256:21c98af08fae4ac9fbcad02b613b6768a4ca300fda4cba867f4a4b6f73c2d04b",
|
"sha256:15cf648dd760a47c55a4106b601b92bb653ae98155b10f04310553629c6695dd",
|
||||||
"sha256:2240372fed30124ddc35b0c15a613f2b687a426ea2f150091e0a0c58cca7a495",
|
"sha256:1d6a011ae4c501c78509caf19cbe152c2e3cb5c267f7b47bc3db8cd3436585a7",
|
||||||
"sha256:2a97f5f1403e058d217d7f6861cf51fca200c5687bce0d052f5f2fa89b5bfa22",
|
"sha256:211f529313953e44ae42eb896c2b688668385e6e8f9d04d21484bddb3c42b34c",
|
||||||
"sha256:3faaefca0ae80d19891acec8b0dd5e6235f59f2206d82375eb80d090285e9557",
|
"sha256:22049ad288d603a7fc68e90a0722770d307886788373ddfe71fbf614ced0f5b2",
|
||||||
"sha256:48ef45b64882901c0d69af3b85d16a19bd0f3e95b43e614fefb53521d8caf36c",
|
"sha256:24f7c371f6ecbee8f0ae30030992fc75cd32cd575dcfca8d466a03a8290377ca",
|
||||||
"sha256:5212fe41f2323fc7356ba67caa39737fe13080562cff37bcbb74a8094076c8d0",
|
"sha256:26cdf561632866d584fedb6b1c1fce78cefa49b5cae54c65aa6a6ca5fe6de4ac",
|
||||||
"sha256:56859c32170663c57bd0658189ce44e180533eebe813853446cd6413810be9eb",
|
"sha256:2c37afcd21a2eb1da1773687e853327fa8ec7d2c5cd90cdcd70180f55f0221e1",
|
||||||
"sha256:5f8fd1cb3478c5534222018aca24fbbd2bc74460c899bda988ec76722c13caa9",
|
"sha256:65b8ec6403814f51e1b9c7e18a8ff26087fcc7a199b1405583e5ff9eb931db56",
|
||||||
"sha256:74300a32c41b3d578772f6933f23a88b19f74484185e71e5225ce2f7ea5aea78",
|
"sha256:66a03103aadb2e2738271cb18c89837ac3980fa0b4687195c4c150228b7e79de",
|
||||||
"sha256:8cbc946bdd217148f4a9c029fcea62f4ae0f67d5346de4c865f4718cd0ddc37f",
|
"sha256:6e8f0124354c53a66f83ec5a18111b760aeff1a64db3a86e7ee5fed8e8624707",
|
||||||
"sha256:9ceefd30076f732530cf84a1be2ecb2fa9931af932706ded760a6d37c73b96ad",
|
"sha256:70f2836cd468aa25bc8b09a2b9561364bd75d3e6ddb0e50a25d248d7da6cff25",
|
||||||
"sha256:ad69c170fda41b07a4c6b668a3128e7a759f50d9aebcfcde0ccff1358abe0423",
|
"sha256:82cebf68952cfb65c86d880eb782a0c558b37531cdae59f2e11fcd0f2bb4669c",
|
||||||
"sha256:b715fe182189fb6870fab5b0383bb2fb278c88c46eade346b0f4c1ed8818c09d",
|
"sha256:84ad3e8fd5f3251fb5b534614da64b04a264ce9348f0fe35b781c0fb378b0f82",
|
||||||
"sha256:bb01ecf95083ffcb9ad542dc5342ccc1059e46f1395fd966629d36d9cc766b4a",
|
"sha256:af13fbc022efa85d1ae161129d4cde66493479db52b9adb74d525b890a078208",
|
||||||
"sha256:bd6328547219cf48cefb4e0a1bc54442910594de1c5a5feae847d9ff3c629031",
|
"sha256:c1d40fb8f8192c75f54f0e74a569ccf45e4e13bed8da78a78a5b488be29979bf",
|
||||||
"sha256:edb128379bb1dea76b5bdbdacf5657a6e4754bacc2049640762725590d8ed905",
|
"sha256:d147ec1ab58512871fdf40a161809f698eaa75720b4a230198e7e028582b20a1",
|
||||||
"sha256:f8e687900557fcd4c51b4e72b9e337fdae9e2c81049d1d80b624bb2e88b5769d",
|
"sha256:dedad1f68d6b0b54000f7f99386351f1c6e19c8cf70a9700d8dd06b9809c54fb",
|
||||||
"sha256:fe0ca120e3347c851c34a91041d574f3c588d832023906d8ae18d66d042e8a52",
|
"sha256:e72c3f5b624b9c7341fd6a7e657926d4cf12a7ea453681ffd7332cabc3530c62",
|
||||||
"sha256:fe8e0152672f24d8bfdecc725f97e9013f2de1b41849150959526ca3562bd3ef"
|
"sha256:eb75f22e261b3bc69b6fc9a17b1d6966c95e79d3e792b7737a018a2bf6a2b07f"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.2.0"
|
"version": "==2.2.5"
|
||||||
},
|
},
|
||||||
"pillow": {
|
"pillow": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:006de60d7580d81f4a1a7e9f0173dc90a932e3905cc4d47ea909bc946302311a",
|
"sha256:165c88bc9d8dba670110c689e3cc5c71dbe4bfb984ffa7cbebf1fac9554071d6",
|
||||||
"sha256:0a2e8d03787ec7ad71dc18aec9367c946ef8ef50e1e78c71f743bc3a770f9fae",
|
"sha256:22d070ca2e60c99929ef274cfced04294d2368193e935c5d6febfd8b601bf865",
|
||||||
"sha256:0eeeae397e5a79dc088d8297a4c2c6f901f8fb30db47795113a4a605d0f1e5ce",
|
"sha256:2353834b2c49b95e1313fb34edf18fca4d57446675d05298bb694bca4b194174",
|
||||||
"sha256:11c5c6e9b02c9dac08af04f093eb5a2f84857df70a7d4a6a6ad461aca803fb9e",
|
"sha256:39725acf2d2e9c17356e6835dccebe7a697db55f25a09207e38b835d5e1bc032",
|
||||||
"sha256:2fb113757a369a6cdb189f8df3226e995acfed0a8919a72416626af1a0a71140",
|
"sha256:3de6b2ee4f78c6b3d89d184ade5d8fa68af0848f9b6b6da2b9ab7943ec46971a",
|
||||||
"sha256:4b0ef2470c4979e345e4e0cc1bbac65fda11d0d7b789dbac035e4c6ce3f98adb",
|
"sha256:47c0d93ee9c8b181f353dbead6530b26980fe4f5485aa18be8f1fd3c3cbc685e",
|
||||||
"sha256:59e903ca800c8cfd1ebe482349ec7c35687b95e98cefae213e271c8c7fffa021",
|
"sha256:5e2fe3bb2363b862671eba632537cd3a823847db4d98be95690b7e382f3d6378",
|
||||||
"sha256:5a3342d34289715928c914ee7f389351eb37fa4857caa9297fc7948f2ed3e53d",
|
"sha256:604815c55fd92e735f9738f65dabf4edc3e79f88541c221d292faec1904a4b17",
|
||||||
"sha256:5abd653a23c35d980b332bc0431d39663b1709d64142e3652890df4c9b6970f6",
|
"sha256:6c5275bd82711cd3dcd0af8ce0bb99113ae8911fc2952805f1d012de7d600a4c",
|
||||||
"sha256:5f9403af9c790cc18411ea398a6950ee2def2a830ad0cfe6dc9122e6d528b302",
|
"sha256:731ca5aabe9085160cf68b2dbef95fc1991015bc0a3a6ea46a371ab88f3d0913",
|
||||||
"sha256:6b4a8fd632b4ebee28282a9fef4c341835a1aa8671e2770b6f89adc8e8c2703c",
|
"sha256:7612520e5e1a371d77e1d1ca3a3ee6227eef00d0a9cddb4ef7ecb0b7396eddf7",
|
||||||
"sha256:6c1aca8231625115104a06e4389fcd9ec88f0c9befbabd80dc206c35561be271",
|
"sha256:7916cbc94f1c6b1301ac04510d0881b9e9feb20ae34094d3615a8a7c3db0dcc0",
|
||||||
"sha256:795e91a60f291e75de2e20e6bdd67770f793c8605b553cb6e4387ce0cb302e09",
|
"sha256:81c3fa9a75d9f1afafdb916d5995633f319db09bd773cb56b8e39f1e98d90820",
|
||||||
"sha256:7ba0ba61252ab23052e642abdb17fd08fdcfdbbf3b74c969a30c58ac1ade7cd3",
|
"sha256:887668e792b7edbfb1d3c9d8b5d8c859269a0f0eba4dda562adb95500f60dbba",
|
||||||
"sha256:7c9401e68730d6c4245b8e361d3d13e1035cbc94db86b49dc7da8bec235d0015",
|
"sha256:8c183b5c60544b49e0a66f924b18c526dfd37774811b627f70836fe01711abd3",
|
||||||
"sha256:81f812d8f5e8a09b246515fac141e9d10113229bc33ea073fec11403b016bcf3",
|
"sha256:93a473b53cc6e0b3ce6bf51b1b95b7b1e7e6084be3a07e40f79b42e83503fbf2",
|
||||||
"sha256:895d54c0ddc78a478c80f9c438579ac15f3e27bf442c2a9aa74d41d0e4d12544",
|
"sha256:96d4dc103d1a0fa6d47c6c55a47de5f5dafd5ef0114fa10c85a1fd8e0216284b",
|
||||||
"sha256:8de332053707c80963b589b22f8e0229f1be1f3ca862a932c1bcd48dafb18dd8",
|
"sha256:a3d3e086474ef12ef13d42e5f9b7bbf09d39cf6bd4940f982263d6954b13f6a9",
|
||||||
"sha256:92c882b70a40c79de9f5294dc99390671e07fc0b0113d472cbea3fde15db1792",
|
"sha256:b02a0b9f332086657852b1f7cb380f6a42403a6d9c42a4c34a561aa4530d5234",
|
||||||
"sha256:95edb1ed513e68bddc2aee3de66ceaf743590bf16c023fb9977adc4be15bd3f0",
|
"sha256:b09e10ec453de97f9a23a5aa5e30b334195e8d2ddd1ce76cc32e52ba63c8b31d",
|
||||||
"sha256:b63d4ff734263ae4ce6593798bcfee6dbfb00523c82753a3a03cbc05555a9cc3",
|
"sha256:b6f00ad5ebe846cc91763b1d0c6d30a8042e02b2316e27b05de04fa6ec831ec5",
|
||||||
"sha256:bd7bf289e05470b1bc74889d1466d9ad4a56d201f24397557b6f65c24a6844b8",
|
"sha256:bba80df38cfc17f490ec651c73bb37cd896bc2400cfba27d078c2135223c1206",
|
||||||
"sha256:cc3ea6b23954da84dbee8025c616040d9aa5eaf34ea6895a0a762ee9d3e12e11",
|
"sha256:c3d911614b008e8a576b8e5303e3db29224b455d3d66d1b2848ba6ca83f9ece9",
|
||||||
"sha256:cc9ec588c6ef3a1325fa032ec14d97b7309db493782ea8c304666fb10c3bd9a7",
|
"sha256:ca20739e303254287138234485579b28cb0d524401f83d5129b5ff9d606cb0a8",
|
||||||
"sha256:d3d07c86d4efa1facdf32aa878bd508c0dc4f87c48125cc16b937baa4e5b5e11",
|
"sha256:cb192176b477d49b0a327b2a5a4979552b7a58cd42037034316b8018ac3ebb59",
|
||||||
"sha256:d8a96747df78cda35980905bf26e72960cba6d355ace4780d4bdde3b217cdf1e",
|
"sha256:cdbbe7dff4a677fb555a54f9bc0450f2a21a93c5ba2b44e09e54fcb72d2bd13d",
|
||||||
"sha256:e38d58d9138ef972fceb7aeec4be02e3f01d383723965bfcef14d174c8ccd039",
|
"sha256:d355502dce85ade85a2511b40b4c61a128902f246504f7de29bbeec1ae27933a",
|
||||||
"sha256:eb472586374dc66b31e36e14720747595c2b265ae962987261f044e5cce644b5",
|
"sha256:dc577f4cfdda354db3ae37a572428a90ffdbe4e51eda7849bf442fb803f09c9b",
|
||||||
"sha256:fbd922f702582cb0d71ef94442bfca57624352622d75e3be7a1e7e9360b07e72"
|
"sha256:dd9eef866c70d2cbbea1ae58134eaffda0d4bfea403025f4db6859724b18ab3d"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==8.0.1"
|
"version": "==8.1.0"
|
||||||
},
|
},
|
||||||
"pluggy": {
|
"pluggy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -574,10 +574,10 @@
|
|||||||
},
|
},
|
||||||
"pytz": {
|
"pytz": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268",
|
"sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4",
|
||||||
"sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd"
|
"sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5"
|
||||||
],
|
],
|
||||||
"version": "==2020.4"
|
"version": "==2020.5"
|
||||||
},
|
},
|
||||||
"redis": {
|
"redis": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -638,50 +638,58 @@
|
|||||||
},
|
},
|
||||||
"reportlab": {
|
"reportlab": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0008b5baa39d7e3a8132c4b47ecae88d6858ad386518e754e5e7b8025ee4722b",
|
"sha256:009fa61710647cdc62eb373345248d8ebb93583a058990f7c4f9be46d90aa5b1",
|
||||||
"sha256:0ad5a540c336941272fe161ef3a9830da3d4b3a65a195531cebd3cad5db58b2a",
|
"sha256:04a08d284da86882ec3a41a7c719833362ef891b09ee8e2fbb47cee352aa684a",
|
||||||
"sha256:0c965a5691686d746f558ee1c52aa9c63a01a0e13cba61ffc661573948e32f61",
|
"sha256:07bff6742fba612da8d1b1f783c436338c6fdc6962828159827d5ca7d2b67935",
|
||||||
"sha256:0fd568fa5615ae99f76289c52ff230207852ee942d4934f6c893c93d2a79544e",
|
"sha256:09fb11ab1500e679fc1b01199d2fed24435499856e75043a9ac0d31dd48fd881",
|
||||||
"sha256:1117d905a3404c696869c7aabec9454b43ed6acbbc73f9256c6fcea23e7ae93e",
|
"sha256:18a876449c9000c391dd3415ebc8454cd7bb9e488977b894886a2d7d018f16cd",
|
||||||
"sha256:1ea7c388e91ad9d823655ad6a13751ff67e8a0e7cf4065cf051b4c931cdd9450",
|
"sha256:18eec161411026dde49767bee4e5e8eeb8014879554811a62581dc7433628d5b",
|
||||||
"sha256:26c0ee8f62652cc7fcdc47a1cb3b34775a4d625738025c1a7edb8718bda5a315",
|
"sha256:19353aead39fc115a4d6c598d6fb9fa26da7e69160a0443ebb49b02903e704e8",
|
||||||
"sha256:368c5b3fc3d5a541cb9dcacefa563fdb445365f517e3cbf64b4326631d1cf13c",
|
"sha256:1b85c20e89c22ae902ca973df2afdd2d64d27dc4ffd2b29ebad8c805a213756b",
|
||||||
"sha256:451d42fdcdd7d84587d6d9c8f5d9a7d0e997305efb606705063ca1fe8bcca551",
|
"sha256:1da3d7a35f918cee905facfa94bd00ae6091cadc06dca1b0b31b69ae02d41d1d",
|
||||||
"sha256:47394acba4da8e56ef8e55d8eb483b868521696ba49ab0f0fcf8a1a4a5ac6e49",
|
"sha256:1e484ce83dae26cb40fcbd312d45b8ba921de7856a00339d867dd4ecf145a1e7",
|
||||||
"sha256:51b16e297f7b937fc530dd151e4b38f1d305b01c9aa10657bc32a5d2901b8ad7",
|
"sha256:33f3cfdc492575f8af3225701301a7e62fc478358729820c9e0091aff5831378",
|
||||||
"sha256:51c0cdcf606ded0a7b4b50050400f25125ea797fbfc3c817135993b38f8b764e",
|
"sha256:3b0026c1129147befd4e5a8cf25da8dea1096fce371e7b2412e36d7254019c06",
|
||||||
"sha256:55c672c579618843e0fd00140fb71f1ffebc4f1c542ac385c4f4999f2f5398d9",
|
"sha256:3d7713dddaa8081ed709a1fa2456a43f6a74b0f07d605da8441fd53fef334f69",
|
||||||
"sha256:5c34a96ecfbf595caf16178a06abcd26a5f8720e01fe1285d4c97333382cfaeb",
|
"sha256:3e2b4d69763103b9dc9b54c0952dc3cee05cedd06e28c0987fad7f84705b12c0",
|
||||||
"sha256:61aa89a00754b18c4f2956b8bff831f1fd3affef6476dc63462d92211941605e",
|
"sha256:4ca5233a19a5ceca23546290f43addec2345789c7d65bb32f8b2668aa148351f",
|
||||||
"sha256:62234d29c97279917903e4587faf240a5dea4617be250db55386ff268eb5a7c5",
|
"sha256:5214a289cf01ebbd65e49bae83709671dd9edb601891cf0ae8abf85f3c0b392f",
|
||||||
"sha256:670f2a8dcc23bf798c39b95c64bf76ee387549b962f76783670821978a226663",
|
"sha256:52f8237654acbc78ea2fa6fb4a6a06e5b023b6da93f7889adfe2deba09473fad",
|
||||||
"sha256:69387f171f6c7b55109caa6d061b17a18f2f9e724a0212c07cd692aeb369dd19",
|
"sha256:5ed00894e0f8281c0b7c0494b4d3067c641fd90c8e5cf933089ec4cc9a48e491",
|
||||||
"sha256:6c5c8871b659f7c2975382d7b61f3c182701fa9eb62cf649c3c73ba8fc5e2595",
|
"sha256:6191961533d49c9d860964d42bada4d7ac3bb28502d984feb8034093f2012fa8",
|
||||||
"sha256:80139ceb3a568f5be908094f1701fd05391b71425e8b69aaed0d30db647ca2aa",
|
"sha256:6f3ad2b1afe99c436563cd436d8693d4a12e2c4bd45f70c7705759ff7837fe53",
|
||||||
"sha256:80661a76d0019b5e2c315ccd3bc7093d754067d6142b36a3a0ec4f416073d23b",
|
"sha256:739b743b7ca1ba4b4d64c321de6fccb49b562d0507ea06c817d9cc4faed5cd22",
|
||||||
"sha256:85a2236f324ae336da7f4b183fa99bed261bcc00ac1255ee91a504e68b086d00",
|
"sha256:792efba0c0c6e4ee94f6dc95f305451733ee9230a1c7d51cb8e5301a549e0dfb",
|
||||||
"sha256:89a3acd98bd4478d6bbc5cb32e0665ea546c98bff8b58d5e1014659daa6ef75a",
|
"sha256:79d63ca40231ca3860859b39a92daa5219035ba9553da89a5e1b218550744121",
|
||||||
"sha256:8a39119fcab146bde41fd1c6d148f9ee1e2cca10c6f9c2b7eb4dd710a3a2c6ac",
|
"sha256:83b28104edd58ad65748d2d0e60e0d97e3b91b3e90b4573ea6fe60de6811972c",
|
||||||
"sha256:9c31c2526401da6cc92018f68483f2aac0a731cb98435445ea4b72d46b438c84",
|
"sha256:85650446538cd2f606ca234634142a7ccd74cb6db7cfec250f76a4242e0f2431",
|
||||||
"sha256:9e8ae1c3b8a1697147c5c97f00d66ab1c54d88c4615b0cdd9b1a667d7baf3eb7",
|
"sha256:8850eba6de6eb813036eb8dce353e40d60c8af48bbce107de82770b10d3aa525",
|
||||||
"sha256:a479c38ab2b997ce05d3bef906783ac20cf4cb224a154e80c9018c5e4d943a35",
|
"sha256:9da445cb79e3f740756924c053edc952cde11a65ff5af8acfda3c0a1317136ef",
|
||||||
"sha256:a79aab8d069543d5085d58260f18705a08acd92a4501a41261913fddc2137d46",
|
"sha256:9fabd5fbd24f5971085ffe53150d663f158f7d3050b25c95736e29ebf676d454",
|
||||||
"sha256:b0a8314383de853599ca531dfe55eaa49bb8d6b0bb663b2f8479b7a0f3385ea2",
|
"sha256:a0c377bc45e73c3f15f55d7de69fab270d174749d5b454ab0de502b15430ec2a",
|
||||||
"sha256:b3d9926e64bd8008007b2d9819d7b30179b069ce95431d5060f71afc36885389",
|
"sha256:a1d3f7022a920d4a5e165d264581f1862e1c1b877ceeabb96fe98cec98125ae5",
|
||||||
"sha256:c2a9a77ce4f25ffb52d705be82a9f41b47f6b0da23870ebc3587709e7242da30",
|
"sha256:a315edef5c5610b0c75790142f49487e89ea34397fc247ae8aa890fe6d6dd057",
|
||||||
"sha256:c578dd0799f70fb577474cd383f035c6e1057e4fe837278113f9cfa6eee4b076",
|
"sha256:a755cca2dcf023130b03bb671670301a992157d5c3151d838c0b68ef89894536",
|
||||||
"sha256:c5abd9d0023ad20030524ab0d5fa39d77aed025519b1fa426304ab2dd0328b89",
|
"sha256:b1b20208ecdfffd7ca027955c4fe8972b28b30a4b3b80cf25099a08d3b20ed7c",
|
||||||
"sha256:ced96125525ba21311e9512adf391170b9e149f89e27e45b06ff07b70f97a0b2",
|
"sha256:b26d6f416891cef93411d6d478a25db275766081a5fb66368248293ef459f3be",
|
||||||
"sha256:d692fb88d6ef5e75242b00009b54953a0425eaa8bd3a36db9db8b396785e1f57",
|
"sha256:b4ba4c30af7044ee987e61c88a5ffb76031ca0c53666bc85d823b7de55ddbc75",
|
||||||
"sha256:d70c2104286459658e61388af9eee838b612986bd8a36e1d21ba36152983ac15",
|
"sha256:b71faf3b6e4d7058e1af1b8afedaf39a962db4a219affc8177009d8244ec10d4",
|
||||||
"sha256:de47c65c10ac6f0d2addb28f1b1657b1c707aca014d09d01b3b728cf19e8f791",
|
"sha256:cfa854bea525f8c913cb77e2bda724d94b965a0eb3bcfc4a645a9baa29bb86e2",
|
||||||
"sha256:e6e7592527791841db0820a72c6afae52655a05b0b6d4df184fd2bafe82ee1ee",
|
"sha256:dd9687359e466086b9f6fe6d8069034017f8b6ca3080944fae5709767ca6814e",
|
||||||
"sha256:e8a7e95ee6ea5566291b59ede5b9fadce809dca43ebfbfe11e3ff3d6492c6f0e",
|
"sha256:de0c675fc2998a7eaa929c356ba49c84f53a892e9ab25e8ee7d8ebbbdcb2ac16",
|
||||||
"sha256:f041759138b3a95508c4281b3db3bf9bb28636d84c554272a58a5ca7c9f9bbf4",
|
"sha256:e2b4e33fea2ce9d3a14ea39191b169e41eb2ac995274f54ac8fd27519974bce8",
|
||||||
"sha256:f39c7fc1fa2e4a1d9747a3effd70731a9d0e9eb5738247fa089c059eff19d43e",
|
"sha256:f3d4a1a273dc141e03b72a553c11bc14dd7a27ec7654a071edcf83eb04f004bc",
|
||||||
"sha256:f65ac89ee0ba569f5279360eae08783f7f2e95c9810a9846c957fbd5950f4896"
|
"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": {
|
"scikit-learn": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -769,13 +777,30 @@
|
|||||||
"markers": "python_version >= '3.5'",
|
"markers": "python_version >= '3.5'",
|
||||||
"version": "==2.1.0"
|
"version": "==2.1.0"
|
||||||
},
|
},
|
||||||
"tqdm": {
|
"tika": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:38b658a3e4ecf9b4f6f8ff75ca16221ae3378b2e175d846b6b33ea3a20852cf5",
|
"sha256:c2c50f405622f74531841104f9e85c17511aede11de8e5385eab1a29a31f191b",
|
||||||
"sha256:d4f413aecb61c9779888c64ddf0c62910ad56dcbe857d8922bb505d4dbff0df1"
|
"sha256:d1f2eddb93caa9a2857569486aa2bc0320d0bf1796cdbe03066954cbc4b4bf62"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"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": {
|
"tzlocal": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -784,13 +809,36 @@
|
|||||||
],
|
],
|
||||||
"version": "==2.1"
|
"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": {
|
"watchdog": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:3caefdcc8f06a57fdc5ef2d22aa7c0bfda4f55e71a0bee74cbf3176d97536ef3",
|
"sha256:016b01495b9c55b5d4126ed8ae75d93ea0d99377084107c33162df52887cee18",
|
||||||
"sha256:e38bffc89b15bafe2a131f0e1c74924cf07dcec020c2e0a26cccd208831fcd43"
|
"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",
|
"index": "pypi",
|
||||||
"version": "==0.10.4"
|
"version": "==1.0.2"
|
||||||
},
|
},
|
||||||
"wcwidth": {
|
"wcwidth": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -873,53 +921,68 @@
|
|||||||
},
|
},
|
||||||
"chardet": {
|
"chardet": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
|
||||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.1'",
|
"markers": "python_version >= '3.1'",
|
||||||
"version": "==3.0.4"
|
"version": "==4.0.0"
|
||||||
},
|
},
|
||||||
"coverage": {
|
"coverage": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516",
|
"sha256:08b3ba72bd981531fd557f67beee376d6700fba183b167857038997ba30dd297",
|
||||||
"sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259",
|
"sha256:262066798d786ad67a13c7ba869e3ce0e39609f99f6d6c80160ad602c4808e32",
|
||||||
"sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9",
|
"sha256:2757fa64e11ec12220968f65d086b7a29b6583d16e9a544c889b22ba98555ef1",
|
||||||
"sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097",
|
"sha256:3102bb2c206700a7d28181dbe04d66b30780cde1d1c02c5f3c165cf3d2489497",
|
||||||
"sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0",
|
"sha256:3498b27d8236057def41de3585f317abae235dd3a11d33e01736ffedb2ef8606",
|
||||||
"sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f",
|
"sha256:378ac77af41350a8c6b8801a66021b52da8a05fd77e578b7380e876c0ce4f528",
|
||||||
"sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7",
|
"sha256:38f16b1317b8dd82df67ed5daa5f5e7c959e46579840d77a67a4ceb9cef0a50b",
|
||||||
"sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c",
|
"sha256:3911c2ef96e5ddc748a3c8b4702c61986628bb719b8378bf1e4a6184bbd48fe4",
|
||||||
"sha256:3188a7dfd96f734a7498f37cde6598b1e9c084f1ca68bc1aa04e88db31168ab6",
|
"sha256:3a3c3f8863255f3c31db3889f8055989527173ef6192a283eb6f4db3c579d830",
|
||||||
"sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5",
|
"sha256:3b14b1da110ea50c8bcbadc3b82c3933974dbeea1832e814aab93ca1163cd4c1",
|
||||||
"sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7",
|
"sha256:535dc1e6e68fad5355f9984d5637c33badbdc987b0c0d303ee95a6c979c9516f",
|
||||||
"sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729",
|
"sha256:6f61319e33222591f885c598e3e24f6a4be3533c1d70c19e0dc59e83a71ce27d",
|
||||||
"sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978",
|
"sha256:723d22d324e7997a651478e9c5a3120a0ecbc9a7e94071f7e1954562a8806cf3",
|
||||||
"sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9",
|
"sha256:76b2775dda7e78680d688daabcb485dc87cf5e3184a0b3e012e1d40e38527cc8",
|
||||||
"sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f",
|
"sha256:782a5c7df9f91979a7a21792e09b34a658058896628217ae6362088b123c8500",
|
||||||
"sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9",
|
"sha256:7e4d159021c2029b958b2363abec4a11db0ce8cd43abb0d9ce44284cb97217e7",
|
||||||
"sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822",
|
"sha256:8dacc4073c359f40fcf73aede8428c35f84639baad7e1b46fce5ab7a8a7be4bb",
|
||||||
"sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418",
|
"sha256:8f33d1156241c43755137288dea619105477961cfa7e47f48dbf96bc2c30720b",
|
||||||
"sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82",
|
"sha256:8ffd4b204d7de77b5dd558cdff986a8274796a1e57813ed005b33fd97e29f059",
|
||||||
"sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f",
|
"sha256:93a280c9eb736a0dcca19296f3c30c720cb41a71b1f9e617f341f0a8e791a69b",
|
||||||
"sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d",
|
"sha256:9a4f66259bdd6964d8cf26142733c81fb562252db74ea367d9beb4f815478e72",
|
||||||
"sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221",
|
"sha256:9a9d4ff06804920388aab69c5ea8a77525cf165356db70131616acd269e19b36",
|
||||||
"sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4",
|
"sha256:a2070c5affdb3a5e751f24208c5c4f3d5f008fa04d28731416e023c93b275277",
|
||||||
"sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21",
|
"sha256:a4857f7e2bc6921dbd487c5c88b84f5633de3e7d416c4dc0bb70256775551a6c",
|
||||||
"sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709",
|
"sha256:a607ae05b6c96057ba86c811d9c43423f35e03874ffb03fbdcd45e0637e8b631",
|
||||||
"sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54",
|
"sha256:a66ca3bdf21c653e47f726ca57f46ba7fc1f260ad99ba783acc3e58e3ebdb9ff",
|
||||||
"sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d",
|
"sha256:ab110c48bc3d97b4d19af41865e14531f300b482da21783fdaacd159251890e8",
|
||||||
"sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270",
|
"sha256:b239711e774c8eb910e9b1ac719f02f5ae4bf35fa0420f438cdc3a7e4e7dd6ec",
|
||||||
"sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24",
|
"sha256:be0416074d7f253865bb67630cf7210cbc14eb05f4099cc0f82430135aaa7a3b",
|
||||||
"sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751",
|
"sha256:c46643970dff9f5c976c6512fd35768c4a3819f01f61169d8cdac3f9290903b7",
|
||||||
"sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a",
|
"sha256:c5ec71fd4a43b6d84ddb88c1df94572479d9a26ef3f150cef3dacefecf888105",
|
||||||
"sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237",
|
"sha256:c6e5174f8ca585755988bc278c8bb5d02d9dc2e971591ef4a1baabdf2d99589b",
|
||||||
"sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7",
|
"sha256:c89b558f8a9a5a6f2cfc923c304d49f0ce629c3bd85cb442ca258ec20366394c",
|
||||||
"sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636",
|
"sha256:cc44e3545d908ecf3e5773266c487ad1877be718d9dc65fc7eb6e7d14960985b",
|
||||||
"sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8",
|
"sha256:cc6f8246e74dd210d7e2b56c76ceaba1cc52b025cd75dbe96eb48791e0250e98",
|
||||||
"sha256:ef221855191457fffeb909d5787d1807800ab4d0111f089e6c93ee68f577634d"
|
"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'",
|
"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": {
|
"coveralls": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -961,19 +1024,19 @@
|
|||||||
},
|
},
|
||||||
"factory-boy": {
|
"factory-boy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:d8626622550c8ba31392f9e19fdbcef9f139cf1ad643c5923f20490a7b3e2e3d",
|
"sha256:1d3db4b44b8c8c54cdd8b83ae4bdb9aeb121e464400035f1f03ae0e1eade56a4",
|
||||||
"sha256:ded73e49135c24bd4d3f45bf1eb168f8d290090f5cf4566b8df3698317dc9c08"
|
"sha256:401cc00ff339a022f84d64a4339503d1689e8263a4478d876e58a3295b155c5b"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.1.0"
|
"version": "==3.2.0"
|
||||||
},
|
},
|
||||||
"faker": {
|
"faker": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1fcb415562ee6e2395b041e85fa6901d4708d30b84d54015226fa754ed0822c3",
|
"sha256:7b0c4bb678be21a68640007f254259c73d18f7996a3448267716423360519732",
|
||||||
"sha256:e8beccb398ee9b8cc1a91d9295121d66512b6753b4846eb1e7370545d46b3311"
|
"sha256:7e98483fc273ec5cfe1c9efa9b99adaa2de4c6b610fbc62d3767088e4974b0ce"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==5.0.1"
|
"version": "==5.3.0"
|
||||||
},
|
},
|
||||||
"filelock": {
|
"filelock": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -1002,19 +1065,19 @@
|
|||||||
},
|
},
|
||||||
"importlib-metadata": {
|
"importlib-metadata": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:6112e21359ef8f344e7178aa5b72dc6e62b38b0d008e6d3cb212c5b84df72013",
|
"sha256:5c5a2720817414a6c41f0a49993908068243ae02c1635a228126519b509c8aed",
|
||||||
"sha256:b0c2d3b226157ae4517d9625decf63591461c66b3a808c2666d538946519d170"
|
"sha256:bf792d480abbd5eda85794e4afb09dd538393f7d6e6ffef6e9f03d2014cf9450"
|
||||||
],
|
],
|
||||||
"markers": "python_version < '3.8'",
|
"markers": "python_version < '3.8'",
|
||||||
"version": "==3.1.1"
|
"version": "==3.3.0"
|
||||||
},
|
},
|
||||||
"importlib-resources": {
|
"importlib-resources": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:7b51f0106c8ec564b1bef3d9c588bc694ce2b92125bbb6278f4f2f5b54ec3592",
|
"sha256:0a948d0c8c3f9344de62997e3f73444dbba233b1eaf24352933c2d264b9e4182",
|
||||||
"sha256:a3d34a8464ce1d5d7c92b0ea4e921e696d86f2aa212e684451cb1482c8d84ed5"
|
"sha256:6b45007a479c4ec21165ae3ffbe37faf35404e2041fac6ae1da684f38530ca73"
|
||||||
],
|
],
|
||||||
"markers": "python_version < '3.7'",
|
"markers": "python_version < '3.7'",
|
||||||
"version": "==3.3.0"
|
"version": "==4.1.1"
|
||||||
},
|
},
|
||||||
"iniconfig": {
|
"iniconfig": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -1077,11 +1140,11 @@
|
|||||||
},
|
},
|
||||||
"packaging": {
|
"packaging": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:05af3bb85d320377db281cf254ab050e1a7ebcbf5410685a9a407e18a1f81236",
|
"sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858",
|
||||||
"sha256:eb41423378682dadb7166144a4926e443093863024de508ca5c9737d6bc08376"
|
"sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
"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": {
|
"pluggy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -1093,11 +1156,11 @@
|
|||||||
},
|
},
|
||||||
"py": {
|
"py": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2",
|
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
|
||||||
"sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"
|
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
"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": {
|
"pycodestyle": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -1125,11 +1188,11 @@
|
|||||||
},
|
},
|
||||||
"pytest": {
|
"pytest": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe",
|
"sha256:1969f797a1a0dbd8ccf0fecc80262312729afea9c17f1d70ebf85c5e76c6f7c8",
|
||||||
"sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e"
|
"sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==6.1.2"
|
"version": "==6.2.1"
|
||||||
},
|
},
|
||||||
"pytest-cov": {
|
"pytest-cov": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -1174,11 +1237,11 @@
|
|||||||
},
|
},
|
||||||
"pytest-xdist": {
|
"pytest-xdist": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:7c629016b3bb006b88ac68e2b31551e7becf173c76b977768848e2bbed594d90",
|
"sha256:1d8edbb1a45e8e1f8e44b1260583107fc23f8bc8da6d18cb331ff61d41258ecf",
|
||||||
"sha256:82d938f1a24186520e2d9d3a64ef7d9ac7ecdf1a0659e095d18e596b8cbd0672"
|
"sha256:f127e11e84ad37cc1de1088cb2990f3c354630d428af3f71282de589c5bb779b"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.1.0"
|
"version": "==2.2.0"
|
||||||
},
|
},
|
||||||
"python-dateutil": {
|
"python-dateutil": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -1190,18 +1253,18 @@
|
|||||||
},
|
},
|
||||||
"pytz": {
|
"pytz": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268",
|
"sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4",
|
||||||
"sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd"
|
"sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5"
|
||||||
],
|
],
|
||||||
"version": "==2020.4"
|
"version": "==2020.5"
|
||||||
},
|
},
|
||||||
"requests": {
|
"requests": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8",
|
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
|
||||||
"sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"
|
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
"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": {
|
"six": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -1220,19 +1283,19 @@
|
|||||||
},
|
},
|
||||||
"sphinx": {
|
"sphinx": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1e8d592225447104d1172be415bc2972bd1357e3e12fdc76edf2261105db4300",
|
"sha256:77dec5ac77ca46eee54f59cf477780f4fb23327b3339ef39c8471abb829c1285",
|
||||||
"sha256:d4e59ad4ea55efbb3c05cde3bfc83bfc14f0c95aa95c3d75346fcce186a47960"
|
"sha256:b8aa4eb5502c53d3b5ca13a07abeedacd887f7770c198952fd5b9530d973e767"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.3.1"
|
"version": "==3.4.2"
|
||||||
},
|
},
|
||||||
"sphinx-rtd-theme": {
|
"sphinx-rtd-theme": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:22c795ba2832a169ca301cd0a083f7a434e09c538c70beb42782c073651b707d",
|
"sha256:eda689eda0c7301a80cf122dad28b1861e5605cbf455558f3775e1e8200e83a5",
|
||||||
"sha256:373413d0f82425aaa28fb288009bf0d0964711d347763af2f1b65cafcb028c82"
|
"sha256:fa6bebd5ab9a73da8e102509a86f3fcc36dec04a0b52ea80e5a033b2aba00113"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.5.0"
|
"version": "==0.5.1"
|
||||||
},
|
},
|
||||||
"sphinxcontrib-applehelp": {
|
"sphinxcontrib-applehelp": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
34
README.md
34
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://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://hub.docker.com/r/jonaswinkler/paperless-ng)
|
||||||
[](https://coveralls.io/github/jonaswinkler/paperless-ng?branch=master)
|
[](https://coveralls.io/github/jonaswinkler/paperless-ng?branch=master)
|
||||||
|
|
||||||
# Paperless-ng
|
# 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.
|
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.
|
||||||
|
|
||||||
@@ -39,14 +40,13 @@ Here's what you get:
|
|||||||
* Auto completion suggests relevant words from your documents.
|
* Auto completion suggests relevant words from your documents.
|
||||||
* Results are sorted by relevance to your search query.
|
* Results are sorted by relevance to your search query.
|
||||||
* Highlighting shows you which parts of the document matched the 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.
|
* Email processing: Paperless adds documents from your email accounts.
|
||||||
* Configure multiple accounts and filters for each account.
|
* 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.
|
* 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.
|
* 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.
|
* 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.
|
* 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).
|
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).
|
||||||
|
|
||||||
@@ -54,11 +54,11 @@ For a complete list of changes from paperless, check out the [changelog](https:/
|
|||||||
|
|
||||||
# Roadmap for 1.0
|
# Roadmap for 1.0
|
||||||
|
|
||||||
- **Bulk editing**. Add/remove metadata from multiple documents at once.
|
|
||||||
|
|
||||||
- Make the front end nice (except mobile).
|
- Make the front end nice (except mobile).
|
||||||
- Test coverage at 90%.
|
|
||||||
- Fix whatever bugs I and you find.
|
- 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
|
## Roadmap for versions beyond 1.0
|
||||||
|
|
||||||
@@ -66,13 +66,11 @@ These are things that I want to add to paperless eventually. They are sorted by
|
|||||||
|
|
||||||
- **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:
|
- **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.
|
- 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.
|
- **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.
|
- **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.
|
- 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.
|
- **Arbitrary tag colors**. Allow the selection of any color with a color picker.
|
||||||
- **More file types**. Possibly allow more file types to be processed by paperless, such as office .odt, .doc, .docx documents.
|
|
||||||
|
|
||||||
Apart from that, paperless is pretty much feature complete.
|
Apart from that, paperless is pretty much feature complete.
|
||||||
|
|
||||||
@@ -80,6 +78,15 @@ Apart from that, paperless is pretty much feature complete.
|
|||||||
|
|
||||||
- **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.
|
- **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
|
# 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).
|
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).
|
||||||
@@ -96,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/).
|
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?
|
# Suggestions? Questions? Something not working?
|
||||||
|
|
||||||
Please open an issue and start a discussion about it!
|
Please open an issue and start a discussion about it!
|
||||||
@@ -116,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.
|
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.
|
* [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.
|
* [paperless-cli](https://github.com/stgarf/paperless-cli): A golang command line binary to interact with a Paperless instance.
|
||||||
|
|
||||||
# Important Note
|
# Important Note
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
bind = '127.0.0.1:8000'
|
bind = '0.0.0.0:8000'
|
||||||
backlog = 2048
|
backlog = 2048
|
||||||
workers = 3
|
workers = 3
|
||||||
worker_class = 'sync'
|
worker_class = 'sync'
|
||||||
|
@@ -15,7 +15,7 @@ services:
|
|||||||
POSTGRES_PASSWORD: paperless
|
POSTGRES_PASSWORD: paperless
|
||||||
|
|
||||||
webserver:
|
webserver:
|
||||||
image: jonaswinkler/paperless-ng:0.9.9
|
image: jonaswinkler/paperless-ng:0.9.13
|
||||||
restart: always
|
restart: always
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
|
@@ -5,7 +5,7 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
webserver:
|
webserver:
|
||||||
image: jonaswinkler/paperless-ng:0.9.9
|
image: jonaswinkler/paperless-ng:0.9.13
|
||||||
restart: always
|
restart: always
|
||||||
depends_on:
|
depends_on:
|
||||||
- broker
|
- 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.13
|
||||||
|
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 \
|
&& apt-get -y --no-install-recommends install \
|
||||||
build-essential \
|
build-essential \
|
||||||
curl \
|
curl \
|
||||||
|
file \
|
||||||
|
fonts-liberation \
|
||||||
|
gettext \
|
||||||
ghostscript \
|
ghostscript \
|
||||||
gnupg \
|
gnupg \
|
||||||
icc-profiles-free \
|
icc-profiles-free \
|
||||||
@@ -20,6 +23,7 @@ RUN apt-get update \
|
|||||||
libpq-dev \
|
libpq-dev \
|
||||||
libqpdf-dev \
|
libqpdf-dev \
|
||||||
libxml2 \
|
libxml2 \
|
||||||
|
libxslt1-dev \
|
||||||
optipng \
|
optipng \
|
||||||
pngquant \
|
pngquant \
|
||||||
qpdf \
|
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 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"]
|
VOLUME ["/usr/src/paperless/data", "/usr/src/paperless/media", "/usr/src/paperless/consume", "/usr/src/paperless/export"]
|
||||||
ENTRYPOINT ["/sbin/docker-entrypoint.sh"]
|
ENTRYPOINT ["/sbin/docker-entrypoint.sh"]
|
||||||
|
EXPOSE 8000
|
||||||
CMD ["/usr/local/bin/supervisord", "-c", "/etc/supervisord.conf"]
|
CMD ["/usr/local/bin/supervisord", "-c", "/etc/supervisord.conf"]
|
||||||
|
|
||||||
LABEL maintainer="Jonas Winkler <dev@jpwinkler.de>"
|
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
|
user=root
|
||||||
|
|
||||||
[program:gunicorn]
|
[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
|
user=paperless
|
||||||
|
|
||||||
stdout_logfile=/dev/stdout
|
stdout_logfile=/dev/stdout
|
||||||
|
@@ -149,6 +149,12 @@ After grabbing the new release and unpacking the contents, do the following:
|
|||||||
$ cd src
|
$ cd src
|
||||||
$ pipenv run python3 manage.py migrate
|
$ pipenv run python3 manage.py migrate
|
||||||
|
|
||||||
|
5. Update translation files.
|
||||||
|
|
||||||
|
.. code:: shell-session
|
||||||
|
|
||||||
|
$ cd src
|
||||||
|
$ pipenv run python3 manage.py compilemessages
|
||||||
|
|
||||||
Management utilities
|
Management utilities
|
||||||
####################
|
####################
|
||||||
|
@@ -5,6 +5,92 @@
|
|||||||
Changelog
|
Changelog
|
||||||
*********
|
*********
|
||||||
|
|
||||||
|
paperless-ng 0.9.13
|
||||||
|
###################
|
||||||
|
|
||||||
|
* Fixed an issue with Paperless not starting due to the new Tika integration when ``USERMAP_UID`` and ``USERMAP_GID`` was used
|
||||||
|
in the ``docker-compose.env`` file.
|
||||||
|
|
||||||
|
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
|
paperless-ng 0.9.9
|
||||||
##################
|
##################
|
||||||
@@ -927,6 +1013,9 @@ bulk of the work on this big change.
|
|||||||
|
|
||||||
* Initial release
|
* Initial release
|
||||||
|
|
||||||
|
.. _slorenz: https://github.com/sisao
|
||||||
|
.. _Jo Vandeginste: https://github.com/jovandeginste
|
||||||
|
.. _zjean: https://github.com/zjean
|
||||||
.. _rYR79435: https://github.com/rYR79435
|
.. _rYR79435: https://github.com/rYR79435
|
||||||
.. _Michael Shamoon: https://github.com/shamoon
|
.. _Michael Shamoon: https://github.com/shamoon
|
||||||
.. _jayme-github: http://github.com/jayme-github
|
.. _jayme-github: http://github.com/jayme-github
|
||||||
|
@@ -162,6 +162,12 @@ PAPERLESS_COOKIE_PREFIX=<str>
|
|||||||
|
|
||||||
Defaults to ``""``, which does not alter the cookie names.
|
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:
|
.. _configuration-ocr:
|
||||||
|
|
||||||
OCR settings
|
OCR settings
|
||||||
@@ -277,6 +283,66 @@ PAPERLESS_OCR_USER_ARG=<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
|
Software tweaks
|
||||||
###############
|
###############
|
||||||
@@ -319,11 +385,14 @@ PAPERLESS_TIME_ZONE=<timezone>
|
|||||||
Defaults to UTC.
|
Defaults to UTC.
|
||||||
|
|
||||||
|
|
||||||
|
.. _configuration-polling:
|
||||||
|
|
||||||
PAPERLESS_CONSUMER_POLLING=<num>
|
PAPERLESS_CONSUMER_POLLING=<num>
|
||||||
If paperless won't find documents added to your consume folder, it might
|
If paperless won't find documents added to your consume folder, it might
|
||||||
not be able to automatically detect filesystem changes. In that case,
|
not be able to automatically detect filesystem changes. In that case,
|
||||||
specify a polling interval in seconds here, which will then cause paperless
|
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.
|
Defaults to 0, which disables polling and uses filesystem notifications.
|
||||||
|
|
||||||
@@ -400,6 +469,28 @@ PAPERLESS_FILENAME_DATE_ORDER=<format>
|
|||||||
|
|
||||||
Defaults to none, which disables this feature.
|
Defaults to none, which disables this feature.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
Binaries
|
Binaries
|
||||||
########
|
########
|
||||||
|
@@ -25,6 +25,8 @@ that works right for you based on recommendations from other Paperless users.
|
|||||||
+---------+----------------+-----+-----+-----+----------------+
|
+---------+----------------+-----+-----+-----+----------------+
|
||||||
| Fujitsu | `ix500`_ | yes | | yes | `eonist`_ |
|
| Fujitsu | `ix500`_ | yes | | yes | `eonist`_ |
|
||||||
+---------+----------------+-----+-----+-----+----------------+
|
+---------+----------------+-----+-----+-----+----------------+
|
||||||
|
| Epson | `WF-7710DWF`_ | yes | | yes | `Skylinar`_ |
|
||||||
|
+---------+----------------+-----+-----+-----+----------------+
|
||||||
| Fujitsu | `S1300i`_ | yes | | yes | `jonaswinkler`_|
|
| 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-J6930DW: https://www.brother.ca/en/p/MFCJ6930DW
|
||||||
.. _MFC-J5910DW: https://www.brother.co.uk/printers/inkjet-printers/mfcj5910dw
|
.. _MFC-J5910DW: https://www.brother.co.uk/printers/inkjet-printers/mfcj5910dw
|
||||||
.. _MFC-9142CDN: https://www.brother.co.uk/printers/laser-printers/mfc9140cdn
|
.. _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/
|
.. _S1300i: https://www.fujitsu.com/global/products/computing/peripheral/scanners/soho/s1300i/
|
||||||
|
|
||||||
.. _danielquinn: https://github.com/danielquinn
|
.. _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
|
.. _bmsleight: https://github.com/bmsleight
|
||||||
.. _eonist: https://github.com/eonist
|
.. _eonist: https://github.com/eonist
|
||||||
.. _REOLDEV: https://github.com/REOLDEV
|
.. _REOLDEV: https://github.com/REOLDEV
|
||||||
|
.. _Skylinar: https://github.com/Skylinar
|
||||||
.. _jonaswinkler: https://github.com/jonaswinkler
|
.. _jonaswinkler: https://github.com/jonaswinkler
|
||||||
|
@@ -180,6 +180,14 @@ Docker Route
|
|||||||
You can use any settings from the file ``paperless.conf`` in this file.
|
You can use any settings from the file ``paperless.conf`` in this file.
|
||||||
Have a look at :ref:`configuration` to see whats available.
|
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
|
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
|
containers. This will also build the image of paperless if you grabbed the
|
||||||
source archive.
|
source archive.
|
||||||
@@ -221,8 +229,9 @@ writing. Windows is not and will never be supported.
|
|||||||
* ``python3-pip``, optionally ``pipenv`` for package installation
|
* ``python3-pip``, optionally ``pipenv`` for package installation
|
||||||
* ``python3-dev``
|
* ``python3-dev``
|
||||||
|
|
||||||
|
* ``fonts-liberation`` for generating thumbnails for plain text files
|
||||||
* ``imagemagick`` >= 6 for PDF conversion
|
* ``imagemagick`` >= 6 for PDF conversion
|
||||||
* ``optipng`` for optimising thumbnails
|
* ``optipng`` for optimizing thumbnails
|
||||||
* ``gnupg`` for handling encrypted documents
|
* ``gnupg`` for handling encrypted documents
|
||||||
* ``libpoppler-cpp-dev`` for PDF to text conversion
|
* ``libpoppler-cpp-dev`` for PDF to text conversion
|
||||||
* ``libmagic-dev`` for mime type detection
|
* ``libmagic-dev`` for mime type detection
|
||||||
@@ -242,8 +251,7 @@ writing. Windows is not and will never be supported.
|
|||||||
* ``tesseract-ocr`` language packs (``tesseract-ocr-eng``, ``tesseract-ocr-deu``, etc)
|
* ``tesseract-ocr`` language packs (``tesseract-ocr-eng``, ``tesseract-ocr-deu``, etc)
|
||||||
|
|
||||||
You will also need ``build-essential``, ``python3-setuptools`` and ``python3-wheel``
|
You will also need ``build-essential``, ``python3-setuptools`` and ``python3-wheel``
|
||||||
for installing some of the python dependencies. You can remove that
|
for installing some of the python dependencies.
|
||||||
again after installation.
|
|
||||||
|
|
||||||
2. Install ``redis`` >= 5.0 and configure it to start automatically.
|
2. Install ``redis`` >= 5.0 and configure it to start automatically.
|
||||||
|
|
||||||
@@ -293,6 +301,9 @@ writing. Windows is not and will never be supported.
|
|||||||
# This creates the database schema.
|
# This creates the database schema.
|
||||||
python3 manage.py migrate
|
python3 manage.py migrate
|
||||||
|
|
||||||
|
# This creates the translation files for paperless.
|
||||||
|
python3 manage.py compilemessages
|
||||||
|
|
||||||
# This creates your first paperless user
|
# This creates your first paperless user
|
||||||
python3 manage.py createsuperuser
|
python3 manage.py createsuperuser
|
||||||
|
|
||||||
|
@@ -34,6 +34,9 @@ directory at startup, but won't find any other files added later, check out
|
|||||||
the configuration file and enable filesystem polling with the setting
|
the configuration file and enable filesystem polling with the setting
|
||||||
``PAPERLESS_CONSUMER_POLLING``.
|
``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
|
Operation not permitted
|
||||||
#######################
|
#######################
|
||||||
|
|
||||||
|
@@ -31,6 +31,7 @@
|
|||||||
#PAPERLESS_STATIC_URL=/static/
|
#PAPERLESS_STATIC_URL=/static/
|
||||||
#PAPERLESS_AUTO_LOGIN_USERNAME=
|
#PAPERLESS_AUTO_LOGIN_USERNAME=
|
||||||
#PAPERLESS_COOKIE_PREFIX=
|
#PAPERLESS_COOKIE_PREFIX=
|
||||||
|
#PAPERLESS_ENABLE_HTTP_REMOTE_USER=false
|
||||||
|
|
||||||
# OCR settings
|
# OCR settings
|
||||||
|
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
#PAPERLESS_OCR_OUTPUT_TYPE=pdfa
|
#PAPERLESS_OCR_OUTPUT_TYPE=pdfa
|
||||||
#PAPERLESS_OCR_PAGES=1
|
#PAPERLESS_OCR_PAGES=1
|
||||||
#PAPERLESS_OCR_IMAGE_DPI=300
|
#PAPERLESS_OCR_IMAGE_DPI=300
|
||||||
#PAPERLESS_OCR_USER_ARG={}
|
#PAPERLESS_OCR_USER_ARGS={}
|
||||||
#PAPERLESS_CONVERT_MEMORY_LIMIT=0
|
#PAPERLESS_CONVERT_MEMORY_LIMIT=0
|
||||||
#PAPERLESS_CONVERT_TMPDIR=/var/tmp/paperless
|
#PAPERLESS_CONVERT_TMPDIR=/var/tmp/paperless
|
||||||
|
|
||||||
@@ -50,10 +51,20 @@
|
|||||||
#PAPERLESS_TIME_ZONE=UTC
|
#PAPERLESS_TIME_ZONE=UTC
|
||||||
#PAPERLESS_CONSUMER_POLLING=10
|
#PAPERLESS_CONSUMER_POLLING=10
|
||||||
#PAPERLESS_CONSUMER_DELETE_DUPLICATES=false
|
#PAPERLESS_CONSUMER_DELETE_DUPLICATES=false
|
||||||
|
#PAPERLESS_CONSUMER_RECURSIVE=false
|
||||||
|
#PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=false
|
||||||
#PAPERLESS_OPTIMIZE_THUMBNAILS=true
|
#PAPERLESS_OPTIMIZE_THUMBNAILS=true
|
||||||
#PAPERLESS_POST_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh
|
#PAPERLESS_POST_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh
|
||||||
#PAPERLESS_FILENAME_DATE_ORDER=YMD
|
#PAPERLESS_FILENAME_DATE_ORDER=YMD
|
||||||
#PAPERLESS_FILENAME_PARSE_TRANSFORMS=[]
|
#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
|
# Binaries
|
||||||
|
|
||||||
|
@@ -57,8 +57,8 @@ pipenv lock --keep-outdated -r > "$PAPERLESS_DIST_APP/requirements.txt"
|
|||||||
# test if the application works.
|
# test if the application works.
|
||||||
|
|
||||||
cd "$PAPERLESS_ROOT/src"
|
cd "$PAPERLESS_ROOT/src"
|
||||||
pipenv run pytest --cov
|
#pipenv run pytest --cov
|
||||||
pipenv run pycodestyle
|
#pipenv run pycodestyle
|
||||||
|
|
||||||
# make the documentation.
|
# 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.
|
# copy python source, templates and static files.
|
||||||
cd "$PAPERLESS_ROOT"
|
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.
|
# 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 -p 5432:5432 -v paperless_pgdata:/var/lib/postgresql/data -d postgres:13
|
||||||
docker run -d -p 6379:6379 redis:latest
|
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": "",
|
"root": "",
|
||||||
"sourceRoot": "src",
|
"sourceRoot": "src",
|
||||||
"prefix": "app",
|
"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": {
|
"architect": {
|
||||||
"build": {
|
"build": {
|
||||||
"builder": "@angular-devkit/build-angular:browser",
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
@@ -23,15 +31,24 @@
|
|||||||
"main": "src/main.ts",
|
"main": "src/main.ts",
|
||||||
"polyfills": "src/polyfills.ts",
|
"polyfills": "src/polyfills.ts",
|
||||||
"tsConfig": "tsconfig.app.json",
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"localize": true,
|
||||||
"aot": true,
|
"aot": true,
|
||||||
"assets": [
|
"assets": [
|
||||||
"src/favicon.ico",
|
"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": [
|
"styles": [
|
||||||
"src/styles.scss"
|
"src/styles.scss"
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": [],
|
||||||
|
"allowedCommonJsDependencies": [
|
||||||
|
"ng2-pdf-viewer"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
@@ -61,13 +78,16 @@
|
|||||||
"maximumError": "10kb"
|
"maximumError": "10kb"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"en-US": {
|
||||||
|
"localize": ["en-US"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"serve": {
|
"serve": {
|
||||||
"builder": "@angular-devkit/build-angular:dev-server",
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "paperless-ui:build"
|
"browserTarget": "paperless-ui:build:en-US"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
@@ -90,7 +110,8 @@
|
|||||||
"karmaConfig": "karma.conf.js",
|
"karmaConfig": "karma.conf.js",
|
||||||
"assets": [
|
"assets": [
|
||||||
"src/favicon.ico",
|
"src/favicon.ico",
|
||||||
"src/assets"
|
"src/assets",
|
||||||
|
"src/manifest.webmanifest"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.scss"
|
"src/styles.scss"
|
||||||
|
1671
src-ui/messages.xlf
Normal file
1671
src-ui/messages.xlf
Normal file
File diff suppressed because it is too large
Load Diff
17
src-ui/package-lock.json
generated
17
src-ui/package-lock.json
generated
@@ -331,6 +331,12 @@
|
|||||||
"ms": "^2.1.1"
|
"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": {
|
"uuid": {
|
||||||
"version": "8.3.0",
|
"version": "8.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz",
|
||||||
@@ -2178,6 +2184,14 @@
|
|||||||
"pacote": "9.5.12",
|
"pacote": "9.5.12",
|
||||||
"semver": "7.3.2",
|
"semver": "7.3.2",
|
||||||
"semver-intersect": "1.4.0"
|
"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": {
|
"@types/glob": {
|
||||||
@@ -6484,8 +6498,7 @@
|
|||||||
},
|
},
|
||||||
"ini": {
|
"ini": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
|
"resolved": "",
|
||||||
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
|
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"inquirer": {
|
"inquirer": {
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { SettingsService } from './services/settings.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@@ -7,8 +8,10 @@ import { Component } from '@angular/core';
|
|||||||
})
|
})
|
||||||
export class AppComponent {
|
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 { PageHeaderComponent } from './components/common/page-header/page-header.component';
|
||||||
import { AppFrameComponent } from './components/app-frame/app-frame.component';
|
import { AppFrameComponent } from './components/app-frame/app-frame.component';
|
||||||
import { ToastsComponent } from './components/common/toasts/toasts.component';
|
import { ToastsComponent } from './components/common/toasts/toasts.component';
|
||||||
import { FilterEditorComponent } from './components/filter-editor/filter-editor.component';
|
import { FilterEditorComponent } from './components/document-list/filter-editor/filter-editor.component';
|
||||||
import { FilterDropdownComponent } from './components/filter-editor/filter-dropdown/filter-dropdown.component';
|
import { FilterableDropdownComponent } from './components/common/filterable-dropdown/filterable-dropdown.component';
|
||||||
import { FilterDropdownButtonComponent } from './components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component';
|
import { ToggleableDropdownButtonComponent } from './components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component';
|
||||||
import { FilterDropdownDateComponent } from './components/filter-editor/filter-dropdown-date/filter-dropdown-date.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 { 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 { 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 { NgxFileDropModule } from 'ngx-file-drop';
|
||||||
import { TextComponent } from './components/common/input/text/text.component';
|
import { TextComponent } from './components/common/input/text/text.component';
|
||||||
import { SelectComponent } from './components/common/input/select/select.component';
|
import { SelectComponent } from './components/common/input/select/select.component';
|
||||||
@@ -54,8 +55,9 @@ import { FileSizePipe } from './pipes/file-size.pipe';
|
|||||||
import { FilterPipe } from './pipes/filter.pipe';
|
import { FilterPipe } from './pipes/filter.pipe';
|
||||||
import { DocumentTitlePipe } from './pipes/document-title.pipe';
|
import { DocumentTitlePipe } from './pipes/document-title.pipe';
|
||||||
import { MetadataCollapseComponent } from './components/document-detail/metadata-collapse/metadata-collapse.component';
|
import { MetadataCollapseComponent } from './components/document-detail/metadata-collapse/metadata-collapse.component';
|
||||||
import { NgSelectModule } from '@ng-select/ng-select';
|
|
||||||
import { SelectDialogComponent } from './components/common/select-dialog/select-dialog.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({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@@ -80,11 +82,12 @@ import { SelectDialogComponent } from './components/common/select-dialog/select-
|
|||||||
AppFrameComponent,
|
AppFrameComponent,
|
||||||
ToastsComponent,
|
ToastsComponent,
|
||||||
FilterEditorComponent,
|
FilterEditorComponent,
|
||||||
FilterDropdownComponent,
|
FilterableDropdownComponent,
|
||||||
FilterDropdownButtonComponent,
|
ToggleableDropdownButtonComponent,
|
||||||
FilterDropdownDateComponent,
|
DateDropdownComponent,
|
||||||
DocumentCardLargeComponent,
|
DocumentCardLargeComponent,
|
||||||
DocumentCardSmallComponent,
|
DocumentCardSmallComponent,
|
||||||
|
BulkEditorComponent,
|
||||||
TextComponent,
|
TextComponent,
|
||||||
SelectComponent,
|
SelectComponent,
|
||||||
CheckComponent,
|
CheckComponent,
|
||||||
@@ -102,7 +105,8 @@ import { SelectDialogComponent } from './components/common/select-dialog/select-
|
|||||||
FilterPipe,
|
FilterPipe,
|
||||||
DocumentTitlePipe,
|
DocumentTitlePipe,
|
||||||
MetadataCollapseComponent,
|
MetadataCollapseComponent,
|
||||||
SelectDialogComponent
|
SelectDialogComponent,
|
||||||
|
NumberComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
|
@@ -1,17 +1,52 @@
|
|||||||
<nav class="navbar navbar-dark sticky-top bg-primary flex-md-nowrap p-0 shadow">
|
<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="#">
|
<button class="navbar-toggler d-md-none collapsed border-0" type="button" data-toggle="collapse"
|
||||||
<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"
|
|
||||||
data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation"
|
data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation"
|
||||||
(click)="isMenuCollapsed = !isMenuCollapsed">
|
(click)="isMenuCollapsed = !isMenuCollapsed">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<form (ngSubmit)="search()" class="w-100 m-1">
|
<a class="navbar-brand col-auto col-md-3 col-lg-2 mr-0 px-3 py-3 order-sm-0" routerLink="/dashboard">
|
||||||
<input class="form-control form-control-dark" type="text" placeholder="Search" aria-label="Search"
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.43 238.91" width="1rem" class="mr-2" fill="currentColor">
|
||||||
[formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (selectItem)="itemSelected($event)">
|
<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>
|
</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>
|
</nav>
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
@@ -28,136 +63,115 @@
|
|||||||
<a class="nav-link" routerLink="dashboard" routerLinkActive="active" (click)="closeMenu()">
|
<a class="nav-link" routerLink="dashboard" routerLinkActive="active" (click)="closeMenu()">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#house"/>
|
<use xlink:href="assets/bootstrap-icons.svg#house"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>Dashboard</ng-container>
|
||||||
Dashboard
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="documents" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" (click)="closeMenu()">
|
<a class="nav-link" routerLink="documents" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" (click)="closeMenu()">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#files"/>
|
<use xlink:href="assets/bootstrap-icons.svg#files"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>Documents</ng-container>
|
||||||
Documents
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</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'>
|
<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>
|
</h6>
|
||||||
<ul class="nav flex-column mb-2">
|
<ul class="nav flex-column mb-2">
|
||||||
<li class="nav-item w-100" *ngFor="let view of savedViewService.sidebarViews">
|
<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()">
|
<a class="nav-link text-truncate" routerLink="view/{{view.id}}" routerLinkActive="active" (click)="closeMenu()">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#funnel"/>
|
<use xlink:href="assets/bootstrap-icons.svg#funnel"/>
|
||||||
</svg>
|
</svg> {{view.name}}
|
||||||
{{view.name}}
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</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'>
|
<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>
|
</h6>
|
||||||
<ul class="nav flex-column mb-2">
|
<ul class="nav flex-column mb-2">
|
||||||
<li class="nav-item w-100" *ngFor='let d of openDocuments'>
|
<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()">
|
<a class="nav-link text-truncate" routerLink="documents/{{d.id}}" routerLinkActive="active" (click)="closeMenu()">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
|
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
|
||||||
</svg>
|
</svg> {{d.title | documentTitle}}
|
||||||
{{d.title | documentTitle}}
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item w-100" *ngIf="openDocuments.length > 1">
|
<li class="nav-item w-100" *ngIf="openDocuments.length > 1">
|
||||||
<a class="nav-link text-truncate" [routerLink]="" (click)="closeAll()">
|
<a class="nav-link text-truncate" [routerLink]="" (click)="closeAll()">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>Close all</ng-container>
|
||||||
Close all
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
<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>
|
</h6>
|
||||||
<ul class="nav flex-column mb-2">
|
<ul class="nav flex-column mb-2">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()">
|
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#person"/>
|
<use xlink:href="assets/bootstrap-icons.svg#person"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>Correspondents</ng-container>
|
||||||
Correspondents
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()">
|
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#tags"/>
|
<use xlink:href="assets/bootstrap-icons.svg#tags"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>Tags</ng-container>
|
||||||
Tags
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()">
|
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#hash"/>
|
<use xlink:href="assets/bootstrap-icons.svg#hash"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>Document types</ng-container>
|
||||||
Document types
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()">
|
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#text-left"/>
|
<use xlink:href="assets/bootstrap-icons.svg#text-left"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>Logs</ng-container>
|
||||||
Logs
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()">
|
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
|
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>Settings</ng-container>
|
||||||
Settings
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="admin/">
|
<a class="nav-link" href="admin/">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#toggles"/>
|
<use xlink:href="assets/bootstrap-icons.svg#toggles"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>Admin</ng-container>
|
||||||
Admin
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
<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>
|
</h6>
|
||||||
<ul class="nav flex-column mb-2">
|
<ul class="nav flex-column mb-2">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" target="_blank" rel="noopener noreferrer" href="https://paperless-ng.readthedocs.io/en/latest/">
|
<a class="nav-link" target="_blank" rel="noopener noreferrer" href="https://paperless-ng.readthedocs.io/en/latest/">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#question-circle"/>
|
<use xlink:href="assets/bootstrap-icons.svg#question-circle"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>Documentation</ng-container>
|
||||||
Documentation
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" target="_blank" rel="noopener noreferrer" href="https://github.com/jonaswinkler/paperless-ng">
|
<a class="nav-link" target="_blank" rel="noopener noreferrer" href="https://github.com/jonaswinkler/paperless-ng">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#link"/>
|
<use xlink:href="assets/bootstrap-icons.svg#link"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>GitHub</ng-container>
|
||||||
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
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@@ -1,36 +1,30 @@
|
|||||||
|
|
||||||
@import "/src/theme";
|
@import "/src/theme";
|
||||||
|
/*
|
||||||
/*
|
|
||||||
* Sidebar
|
* Sidebar
|
||||||
*/
|
*/
|
||||||
|
.sidebar {
|
||||||
.sidebar {
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 100; /* Behind the navbar */
|
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);
|
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
.sidebar {
|
.sidebar {
|
||||||
top: 3rem;
|
top: 3.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-sticky {
|
.sidebar-sticky {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 0;
|
top: 0;
|
||||||
/* height: calc(100vh - 48px); */
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding-top: .5rem;
|
padding-top: 0.5rem;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
|
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
|
||||||
}
|
}
|
||||||
|
|
||||||
@supports ((position: -webkit-sticky) or (position: sticky)) {
|
@supports ((position: -webkit-sticky) or (position: sticky)) {
|
||||||
.sidebar-sticky {
|
.sidebar-sticky {
|
||||||
position: -webkit-sticky;
|
position: -webkit-sticky;
|
||||||
@@ -53,36 +47,85 @@
|
|||||||
font-weight: bold;
|
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;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-heading {
|
.sidebar-heading {
|
||||||
font-size: .75rem;
|
font-size: 0.75rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Navbar
|
* Navbar
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
padding-top: .75rem;
|
padding-top: 0.75rem;
|
||||||
padding-bottom: .75rem;
|
padding-bottom: 0.75rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
background-color: rgba(0, 0, 0, .25);
|
|
||||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar .navbar-toggler {
|
.dropdown.show .dropdown-toggle,
|
||||||
top: .25rem;
|
.dropdown-toggle:hover {
|
||||||
right: 1rem;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar .form-control {
|
.dropdown-toggle::after {
|
||||||
padding: .75rem 1rem;
|
margin-left: 0.4em;
|
||||||
border-width: 0;
|
vertical-align: 0.155em;
|
||||||
border-radius: 0;
|
}
|
||||||
|
|
||||||
|
.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,6 +9,7 @@ import { SavedViewService } from 'src/app/services/rest/saved-view.service';
|
|||||||
import { SearchService } from 'src/app/services/rest/search.service';
|
import { SearchService } from 'src/app/services/rest/search.service';
|
||||||
import { environment } from 'src/environments/environment';
|
import { environment } from 'src/environments/environment';
|
||||||
import { DocumentDetailComponent } from '../document-detail/document-detail.component';
|
import { DocumentDetailComponent } from '../document-detail/document-detail.component';
|
||||||
|
import { Meta } from '@angular/platform-browser';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-app-frame',
|
selector: 'app-app-frame',
|
||||||
@@ -22,8 +23,10 @@ export class AppFrameComponent implements OnInit, OnDestroy {
|
|||||||
private activatedRoute: ActivatedRoute,
|
private activatedRoute: ActivatedRoute,
|
||||||
private openDocumentsService: OpenDocumentsService,
|
private openDocumentsService: OpenDocumentsService,
|
||||||
private searchService: SearchService,
|
private searchService: SearchService,
|
||||||
public savedViewService: SavedViewService
|
public savedViewService: SavedViewService,
|
||||||
|
private meta: Meta
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
versionString = `${environment.appTitle} ${environment.version}`
|
versionString = `${environment.appTitle} ${environment.version}`
|
||||||
@@ -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,8 +9,8 @@
|
|||||||
<p *ngIf="message">{{message}}</p>
|
<p *ngIf="message">{{message}}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-dark" (click)="cancelClicked()">Cancel</button>
|
<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">
|
<button type="button" class="btn" [class]="btnClass" (click)="confirmClicked.emit()" [disabled]="!confirmButtonEnabled || !buttonsEnabled">
|
||||||
{{btnCaption}}
|
{{btnCaption}}
|
||||||
<span *ngIf="!confirmButtonEnabled"> ({{seconds}})</span>
|
<span *ngIf="!confirmButtonEnabled"> ({{seconds}})</span>
|
||||||
</button>
|
</button>
|
||||||
|
@@ -14,7 +14,7 @@ export class ConfirmDialogComponent implements OnInit {
|
|||||||
public confirmClicked = new EventEmitter()
|
public confirmClicked = new EventEmitter()
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
title = "Confirmation"
|
title = $localize`Confirmation`
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
messageBold
|
messageBold
|
||||||
@@ -26,7 +26,10 @@ export class ConfirmDialogComponent implements OnInit {
|
|||||||
btnClass = "btn-primary"
|
btnClass = "btn-primary"
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
btnCaption = "Confirm"
|
btnCaption = $localize`Confirm`
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
buttonsEnabled = true
|
||||||
|
|
||||||
confirmButtonEnabled = true
|
confirmButtonEnabled = true
|
||||||
seconds = 0
|
seconds = 0
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="dateBefore || dateAfter ? 'btn-primary' : 'btn-outline-primary'">
|
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="dateBefore || dateAfter ? 'btn-primary' : 'btn-outline-primary'">
|
||||||
{{title}}
|
{{title}}
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-menu date-filter shadow pt-0" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
|
<div class="dropdown-menu date-dropdown shadow pt-0" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
|
||||||
<div class="list-group list-group-flush">
|
<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)">
|
<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}}
|
{{qf.name}}
|
||||||
@@ -10,12 +10,12 @@
|
|||||||
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
|
<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 class="mb-2 d-flex flex-row w-100 justify-content-between small">
|
||||||
<div>After</div>
|
<div i18n>After</div>
|
||||||
<a *ngIf="dateAfter" class="btn btn-link p-0 m-0" (click)="clearAfter()">
|
<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">
|
<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" />
|
<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>
|
</svg>
|
||||||
<small>Clear</small>
|
<small i18n>Clear</small>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -26,12 +26,12 @@
|
|||||||
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
|
<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 class="mb-2 d-flex flex-row w-100 justify-content-between small">
|
||||||
<div>Before</div>
|
<div i18n>Before</div>
|
||||||
<a *ngIf="dateBefore" class="btn btn-link p-0 m-0" (click)="clearBefore()">
|
<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">
|
<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" />
|
<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>
|
</svg>
|
||||||
<small>Clear</small>
|
<small i18n>Clear</small>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@@ -1,4 +1,4 @@
|
|||||||
.date-filter {
|
.date-dropdown {
|
||||||
min-width: 250px;
|
min-width: 250px;
|
||||||
|
|
||||||
.btn-link {
|
.btn-link {
|
@@ -1,20 +1,20 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { FilterDropodownComponent } from './filter-dropdown.component';
|
import { DateDropdownComponent } from './date-dropdown.component';
|
||||||
|
|
||||||
describe('FilterDropodownComponent', () => {
|
describe('DateDropdownComponent', () => {
|
||||||
let component: FilterDropodownComponent;
|
let component: DateDropdownComponent;
|
||||||
let fixture: ComponentFixture<FilterDropodownComponent>;
|
let fixture: ComponentFixture<DateDropdownComponent>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [ FilterDropodownComponent ]
|
declarations: [ DateDropdownComponent ]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(FilterDropodownComponent);
|
fixture = TestBed.createComponent(DateDropdownComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
@@ -8,31 +8,37 @@ export interface DateSelection {
|
|||||||
after?: string
|
after?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const FILTER_LAST_7_DAYS = 0
|
const LAST_7_DAYS = 0
|
||||||
const FILTER_LAST_MONTH = 1
|
const LAST_MONTH = 1
|
||||||
const FILTER_LAST_3_MONTHS = 2
|
const LAST_3_MONTHS = 2
|
||||||
const FILTER_LAST_YEAR = 3
|
const LAST_YEAR = 3
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-filter-dropdown-date',
|
selector: 'app-date-dropdown',
|
||||||
templateUrl: './filter-dropdown-date.component.html',
|
templateUrl: './date-dropdown.component.html',
|
||||||
styleUrls: ['./filter-dropdown-date.component.scss']
|
styleUrls: ['./date-dropdown.component.scss']
|
||||||
})
|
})
|
||||||
export class FilterDropdownDateComponent implements OnInit, OnDestroy {
|
export class DateDropdownComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
quickFilters = [
|
quickFilters = [
|
||||||
{id: FILTER_LAST_7_DAYS, name: "Last 7 days"},
|
{id: LAST_7_DAYS, name: $localize`Last 7 days`},
|
||||||
{id: FILTER_LAST_MONTH, name: "Last month"},
|
{id: LAST_MONTH, name: $localize`Last month`},
|
||||||
{id: FILTER_LAST_3_MONTHS, name: "Last 3 months"},
|
{id: LAST_3_MONTHS, name: $localize`Last 3 months`},
|
||||||
{id: FILTER_LAST_YEAR, name: "Last year"}
|
{id: LAST_YEAR, name: $localize`Last year`}
|
||||||
]
|
]
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
dateBefore: string
|
dateBefore: string
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
dateBeforeChange = new EventEmitter<string>()
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
dateAfter: string
|
dateAfter: string
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
dateAfterChange = new EventEmitter<string>()
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
title: string
|
title: string
|
||||||
|
|
||||||
@@ -61,19 +67,19 @@ export class FilterDropdownDateComponent implements OnInit, OnDestroy {
|
|||||||
this.dateBefore = null
|
this.dateBefore = null
|
||||||
let date = new Date()
|
let date = new Date()
|
||||||
switch (qf) {
|
switch (qf) {
|
||||||
case FILTER_LAST_7_DAYS:
|
case LAST_7_DAYS:
|
||||||
date.setDate(date.getDate() - 7)
|
date.setDate(date.getDate() - 7)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case FILTER_LAST_MONTH:
|
case LAST_MONTH:
|
||||||
date.setMonth(date.getMonth() - 1)
|
date.setMonth(date.getMonth() - 1)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case FILTER_LAST_3_MONTHS:
|
case LAST_3_MONTHS:
|
||||||
date.setMonth(date.getMonth() - 3)
|
date.setMonth(date.getMonth() - 3)
|
||||||
break
|
break
|
||||||
|
|
||||||
case FILTER_LAST_YEAR:
|
case LAST_YEAR:
|
||||||
date.setFullYear(date.getFullYear() - 1)
|
date.setFullYear(date.getFullYear() - 1)
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -83,6 +89,8 @@ export class FilterDropdownDateComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onChange() {
|
onChange() {
|
||||||
|
this.dateAfterChange.emit(this.dateAfter)
|
||||||
|
this.dateBeforeChange.emit(this.dateBefore)
|
||||||
this.datesSet.emit({after: this.dateAfter, before: this.dateBefore})
|
this.datesSet.emit({after: this.dateAfter, before: this.dateBefore})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,12 +99,12 @@ export class FilterDropdownDateComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearBefore() {
|
clearBefore() {
|
||||||
this.dateBefore = null;
|
this.dateBefore = null
|
||||||
this.onChange()
|
this.onChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
clearAfter() {
|
clearAfter() {
|
||||||
this.dateAfter = null;
|
this.dateAfter = null
|
||||||
this.onChange()
|
this.onChange()
|
||||||
}
|
}
|
||||||
|
|
@@ -2,10 +2,11 @@ import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
|||||||
import { FormGroup } from '@angular/forms';
|
import { FormGroup } from '@angular/forms';
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { Observable } from 'rxjs';
|
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 { ObjectWithId } from 'src/app/data/object-with-id';
|
||||||
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service';
|
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()
|
@Directive()
|
||||||
export abstract class EditDialogComponent<T extends ObjectWithId> implements OnInit {
|
export abstract class EditDialogComponent<T extends ObjectWithId> implements OnInit {
|
||||||
@@ -13,8 +14,7 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI
|
|||||||
constructor(
|
constructor(
|
||||||
private service: AbstractPaperlessService<T>,
|
private service: AbstractPaperlessService<T>,
|
||||||
private activeModal: NgbActiveModal,
|
private activeModal: NgbActiveModal,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService) { }
|
||||||
private entityName: string) { }
|
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
dialogMode: string = 'create'
|
dialogMode: string = 'create'
|
||||||
@@ -25,6 +25,10 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI
|
|||||||
@Output()
|
@Output()
|
||||||
success = new EventEmitter()
|
success = new EventEmitter()
|
||||||
|
|
||||||
|
networkActive = false
|
||||||
|
|
||||||
|
error = null
|
||||||
|
|
||||||
abstract getForm(): FormGroup
|
abstract getForm(): FormGroup
|
||||||
|
|
||||||
objectForm: FormGroup = this.getForm()
|
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() {
|
getTitle() {
|
||||||
switch (this.dialogMode) {
|
switch (this.dialogMode) {
|
||||||
case 'create':
|
case 'create':
|
||||||
return "Create new " + this.entityName
|
return this.getCreateTitle()
|
||||||
case 'edit':
|
case 'edit':
|
||||||
return "Edit " + this.entityName
|
return this.getEditTitle()
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -50,6 +66,10 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI
|
|||||||
return MATCHING_ALGORITHMS
|
return MATCHING_ALGORITHMS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get patternRequired(): boolean {
|
||||||
|
return this.objectForm?.value.matching_algorithm !== MATCH_AUTO
|
||||||
|
}
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
var newObject = Object.assign(Object.assign({}, this.object), this.objectForm.value)
|
var newObject = Object.assign(Object.assign({}, this.object), this.objectForm.value)
|
||||||
var serverResponse: Observable<T>
|
var serverResponse: Observable<T>
|
||||||
@@ -62,11 +82,13 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
this.networkActive = true
|
||||||
serverResponse.subscribe(result => {
|
serverResponse.subscribe(result => {
|
||||||
this.activeModal.close()
|
this.activeModal.close()
|
||||||
this.success.emit(result)
|
this.success.emit(result)
|
||||||
}, error => {
|
}, 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 { ControlValueAccessor } from '@angular/forms';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export class AbstractInputComponent<T> implements OnInit, ControlValueAccessor {
|
export class AbstractInputComponent<T> implements OnInit, ControlValueAccessor {
|
||||||
|
|
||||||
|
@ViewChild("inputField")
|
||||||
|
inputField: ElementRef
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
onChange = (newValue: T) => {};
|
onChange = (newValue: T) => {};
|
||||||
@@ -24,12 +27,21 @@ export class AbstractInputComponent<T> implements OnInit, ControlValueAccessor {
|
|||||||
this.disabled = isDisabled;
|
this.disabled = isDisabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
if (this.inputField && this.inputField.nativeElement) {
|
||||||
|
this.inputField.nativeElement.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
title: string
|
title: string
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
disabled = false;
|
disabled = false;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
error: string
|
||||||
|
|
||||||
value: T
|
value: T
|
||||||
|
|
||||||
ngOnInit(): void {
|
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 { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { FilterDropdownDateComponent } from './filter-dropdown-date.component';
|
import { NumberComponent } from './number.component';
|
||||||
|
|
||||||
describe('FilterDropdownDateComponent', () => {
|
describe('NumberComponent', () => {
|
||||||
let component: FilterDropdownDateComponent;
|
let component: NumberComponent;
|
||||||
let fixture: ComponentFixture<FilterDropdownDateComponent>;
|
let fixture: ComponentFixture<NumberComponent>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [ FilterDropdownDateComponent ]
|
declarations: [ NumberComponent ]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(FilterDropdownDateComponent);
|
fixture = TestBed.createComponent(NumberComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
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,13 @@
|
|||||||
<div class="form-group paperless-input-select paperless-input-tags">
|
<div class="form-group paperless-input-select paperless-input-tags">
|
||||||
<label for="tags">Tags</label>
|
<label for="tags" i18n>Tags</label>
|
||||||
|
|
||||||
<div class="input-group flex-nowrap">
|
<div class="input-group flex-nowrap">
|
||||||
<ng-select name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="displayValue"
|
<ng-select name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="displayValue"
|
||||||
[multiple]="true"
|
[multiple]="true"
|
||||||
[closeOnSelect]="false"
|
[closeOnSelect]="false"
|
||||||
|
[clearSearchOnAdd]="true"
|
||||||
[disabled]="disabled"
|
[disabled]="disabled"
|
||||||
|
[hideSelected]="true"
|
||||||
(change)="ngSelectChange()">
|
(change)="ngSelectChange()">
|
||||||
|
|
||||||
<ng-template ng-label-tmp let-item="item">
|
<ng-template ng-label-tmp let-item="item">
|
||||||
|
@@ -1,5 +1,8 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label [for]="inputId">{{title}}</label>
|
<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>
|
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
{{error}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
@@ -10,6 +10,6 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-dark" (click)="cancelClicked()">Cancel</button>
|
<button type="button" class="btn btn-outline-dark" (click)="cancelClicked()" i18n>Cancel</button>
|
||||||
<button type="button" class="btn btn-primary" (click)="selectClicked.emit(selected)">Select</button>
|
<button type="button" class="btn btn-primary" (click)="selectClicked.emit(selected)" i18n>Select</button>
|
||||||
</div>
|
</div>
|
@@ -15,10 +15,10 @@ export class SelectDialogComponent implements OnInit {
|
|||||||
public selectClicked = new EventEmitter()
|
public selectClicked = new EventEmitter()
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
title = "Select"
|
title = $localize`Select`
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
message = "Please select an object"
|
message = $localize`Please select an object`
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
objects: ObjectWithId[] = []
|
objects: ObjectWithId[] = []
|
||||||
|
File diff suppressed because one or more lines are too long
@@ -30,9 +30,9 @@ export class DashboardComponent implements OnInit {
|
|||||||
|
|
||||||
get subtitle() {
|
get subtitle() {
|
||||||
if (this.displayName) {
|
if (this.displayName) {
|
||||||
return `Hello ${this.displayName}, welcome to Paperless-ng!`
|
return $localize`Hello ${this.displayName}, welcome to Paperless-ng!`
|
||||||
} else {
|
} else {
|
||||||
return `Welcome to Paperless-ng!`
|
return $localize`Welcome to Paperless-ng!`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
<app-widget-frame [title]="savedView.name">
|
<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">
|
<table content class="table table-sm table-hover table-borderless">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Created</th>
|
<th i18n>Created</th>
|
||||||
<th scope="col">Title</th>
|
<th scope="col" i18n>Title</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@@ -0,0 +1,7 @@
|
|||||||
|
table {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
th:first-child {
|
||||||
|
min-width: 5rem;
|
||||||
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<app-widget-frame title="Statistics">
|
<app-widget-frame title="Statistics" i18n-title>
|
||||||
<ng-container content>
|
<ng-container content>
|
||||||
<p class="card-text">Documents in inbox: {{statistics.documents_inbox}}</p>
|
<p class="card-text" i18n>Documents in inbox: {{statistics.documents_inbox}}</p>
|
||||||
<p class="card-text">Total documents: {{statistics.documents_total}}</p>
|
<p class="card-text" i18n>Total documents: {{statistics.documents_total}}</p>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</app-widget-frame>
|
</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>
|
<div content>
|
||||||
<form>
|
<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"
|
(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
|
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>
|
</ngx-file-drop>
|
||||||
</form>
|
</form>
|
||||||
<div *ngIf="uploadVisible" class="mt-3">
|
<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 [value]="loadedSum" [max]="totalSum" [striped]="true" [animated]="uploadStatus.length > 0">
|
||||||
</ngb-progressbar>
|
</ngb-progressbar>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -2,7 +2,7 @@ import { HttpEventType } from '@angular/common/http';
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop';
|
import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop';
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service';
|
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 {
|
interface UploadStatus {
|
||||||
@@ -60,7 +60,7 @@ export class UploadFileWidgetComponent implements OnInit {
|
|||||||
} else if (event.type == HttpEventType.Response) {
|
} else if (event.type == HttpEventType.Response) {
|
||||||
this.uploadStatus.splice(this.uploadStatus.indexOf(uploadStatusObject), 1)
|
this.uploadStatus.splice(this.uploadStatus.indexOf(uploadStatusObject), 1)
|
||||||
this.completedFiles += 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 => {
|
}, error => {
|
||||||
@@ -68,11 +68,11 @@ export class UploadFileWidgetComponent implements OnInit {
|
|||||||
this.completedFiles += 1
|
this.completedFiles += 1
|
||||||
switch (error.status) {
|
switch (error.status) {
|
||||||
case 400: {
|
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;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,16 +1,16 @@
|
|||||||
<app-widget-frame title="First steps">
|
<app-widget-frame title="First steps" i18n-title>
|
||||||
|
|
||||||
<ng-container content>
|
<ng-container content>
|
||||||
<img src="assets/save-filter.png" class="float-right">
|
<img src="assets/save-filter.png" class="float-right">
|
||||||
<p>Paperless is running! :)</p>
|
<p i18n>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.
|
<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 have them displayed on the dashboard instead of this message.</p>
|
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>Paperless offers some more features that try to make your life easier, such as:</p>
|
<p i18n>Paperless offers some more features that try to make your life easier:</p>
|
||||||
<ul>
|
<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 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>You can configure paperless to read your mails and add documents from attached files.</li>
|
<li i18n>You can configure paperless to read your mails and add documents from attached files.</li>
|
||||||
</ul>
|
</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>
|
</ng-container>
|
||||||
|
|
||||||
</app-widget-frame>
|
</app-widget-frame>
|
@@ -1,19 +1,18 @@
|
|||||||
<app-page-header [(title)]="title">
|
<app-page-header [(title)]="title">
|
||||||
<div class="input-group input-group-sm mr-5" *ngIf="getContentType() == 'application/pdf'">
|
<div class="input-group input-group-sm mr-5" *ngIf="getContentType() == 'application/pdf'">
|
||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
<div class="input-group-text">Page </div>
|
<div class="input-group-text" i18n>Page</div>
|
||||||
</div>
|
</div>
|
||||||
<input class="form-control flex-grow-0 w-auto" type="number" min="1" [max]="previewNumPages" [(ngModel)]="previewCurrentPage" />
|
<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-append">
|
||||||
<div class="input-group-text">of {{previewNumPages}}</div>
|
<div class="input-group-text" i18n>of {{previewNumPages}}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger mr-2" (click)="delete()">
|
<button type="button" class="btn btn-sm btn-outline-danger mr-2" (click)="delete()">
|
||||||
<svg class="buttonicon" fill="currentColor">
|
<svg class="buttonicon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||||
</svg>
|
</svg> <span class="d-none d-lg-inline" i18n>Delete</span>
|
||||||
<span class="d-none d-lg-inline"> Delete</span>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="btn-group mr-2">
|
<div class="btn-group mr-2">
|
||||||
@@ -21,14 +20,13 @@
|
|||||||
<a [href]="downloadUrl" class="btn btn-sm btn-outline-primary">
|
<a [href]="downloadUrl" class="btn btn-sm btn-outline-primary">
|
||||||
<svg class="buttonicon" fill="currentColor">
|
<svg class="buttonicon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#download" />
|
<use xlink:href="assets/bootstrap-icons.svg#download" />
|
||||||
</svg>
|
</svg> <span class="d-none d-lg-inline" i18n>Download</span>
|
||||||
<span class="d-none d-lg-inline"> Download</span>
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="btn-group" ngbDropdown role="group" *ngIf="metadata?.has_archive_version">
|
<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>
|
<button class="btn btn-sm btn-outline-primary dropdown-toggle-split" ngbDropdownToggle></button>
|
||||||
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
||||||
<a ngbDropdownItem [href]="downloadOriginalUrl">Download original</a>
|
<a ngbDropdownItem [href]="downloadOriginalUrl" i18n>Download original</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -37,15 +35,13 @@
|
|||||||
<button type="button" class="btn btn-sm btn-outline-primary mr-2" (click)="moreLike()">
|
<button type="button" class="btn btn-sm btn-outline-primary mr-2" (click)="moreLike()">
|
||||||
<svg class="buttonicon" fill="currentColor">
|
<svg class="buttonicon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#three-dots" />
|
<use xlink:href="assets/bootstrap-icons.svg#three-dots" />
|
||||||
</svg>
|
</svg> <span class="d-none d-lg-inline" i18n>More like this</span>
|
||||||
<span class="d-none d-lg-inline"> More like this</span>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="close()">
|
<button type="button" class="btn btn-sm btn-outline-primary" (click)="close()">
|
||||||
<svg class="buttonicon" fill="currentColor">
|
<svg class="buttonicon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#x" />
|
<use xlink:href="assets/bootstrap-icons.svg#x" />
|
||||||
</svg>
|
</svg> <span class="d-none d-lg-inline" i18n>Close</span>
|
||||||
<span class="d-none d-lg-inline"> Close</span>
|
|
||||||
</button>
|
</button>
|
||||||
</app-page-header>
|
</app-page-header>
|
||||||
|
|
||||||
@@ -57,27 +53,23 @@
|
|||||||
|
|
||||||
<ul ngbNav #nav="ngbNav" class="nav-tabs">
|
<ul ngbNav #nav="ngbNav" class="nav-tabs">
|
||||||
<li [ngbNavItem]="1">
|
<li [ngbNavItem]="1">
|
||||||
<a ngbNavLink>Details</a>
|
<a ngbNavLink i18n>Details</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
|
|
||||||
<app-input-text title="Title" formControlName="title"></app-input-text>
|
<app-input-text #inputTitle i18n-title title="Title" formControlName="title" [error]="error?.title"></app-input-text>
|
||||||
<div class="form-group">
|
<app-input-number i18n-title title="Archive serial number" [error]="error?.archive_serial_number" formControlName='archive_serial_number'></app-input-number>
|
||||||
<label for="archive_serial_number">Archive Serial Number</label>
|
<app-input-date-time i18n-titleDate titleDate="Date created" formControlName="created"></app-input-date-time>
|
||||||
<input type="number" class="form-control" id="archive_serial_number"
|
<app-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true"
|
||||||
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>
|
(createNew)="createCorrespondent()"></app-input-select>
|
||||||
<app-input-select [items]="documentTypes" title="Document type" formControlName="document_type" [allowNull]="true"
|
<app-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true"
|
||||||
(createNew)="createDocumentType()"></app-input-select>
|
(createNew)="createDocumentType()"></app-input-select>
|
||||||
<app-input-tags formControlName="tags" title="Tags"></app-input-tags>
|
<app-input-tags formControlName="tags"></app-input-tags>
|
||||||
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li [ngbNavItem]="2">
|
<li [ngbNavItem]="2">
|
||||||
<a ngbNavLink>Content</a>
|
<a ngbNavLink i18n>Content</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<textarea class="form-control" id="content" rows="20" formControlName='content'></textarea>
|
<textarea class="form-control" id="content" rows="20" formControlName='content'></textarea>
|
||||||
@@ -86,48 +78,48 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li [ngbNavItem]="3">
|
<li [ngbNavItem]="3">
|
||||||
<a ngbNavLink>Metadata</a>
|
<a ngbNavLink i18n>Metadata</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
|
|
||||||
<table class="table table-borderless">
|
<table class="table table-borderless">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Date modified</td>
|
<td i18n>Date modified</td>
|
||||||
<td>{{document.modified | date:'medium'}}</td>
|
<td>{{document.modified | date:'medium'}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Date added</td>
|
<td i18n>Date added</td>
|
||||||
<td>{{document.added | date:'medium'}}</td>
|
<td>{{document.added | date:'medium'}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Media filename</td>
|
<td i18n>Media filename</td>
|
||||||
<td>{{metadata?.media_filename}}</td>
|
<td>{{metadata?.media_filename}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Original MD5 Checksum</td>
|
<td i18n>Original MD5 checksum</td>
|
||||||
<td>{{metadata?.original_checksum}}</td>
|
<td>{{metadata?.original_checksum}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Original file size</td>
|
<td i18n>Original file size</td>
|
||||||
<td>{{metadata?.original_size | fileSize}}</td>
|
<td>{{metadata?.original_size | fileSize}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Original mime type</td>
|
<td i18n>Original mime type</td>
|
||||||
<td>{{metadata?.original_mime_type}}</td>
|
<td>{{metadata?.original_mime_type}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="metadata?.has_archive_version">
|
<tr *ngIf="metadata?.has_archive_version">
|
||||||
<td>Archive MD5 Checksum</td>
|
<td i18n>Archive MD5 checksum</td>
|
||||||
<td>{{metadata?.archive_checksum}}</td>
|
<td>{{metadata?.archive_checksum}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="metadata?.has_archive_version">
|
<tr *ngIf="metadata?.has_archive_version">
|
||||||
<td>Archive file size</td>
|
<td i18n>Archive file size</td>
|
||||||
<td>{{metadata?.archive_size | fileSize}}</td>
|
<td>{{metadata?.archive_size | fileSize}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<app-metadata-collapse title="Original document metadata" [metadata]="metadata.original_metadata" *ngIf="metadata?.original_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 title="Archived document metadata" [metadata]="metadata.archive_metadata" *ngIf="metadata?.archive_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>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
@@ -135,16 +127,15 @@
|
|||||||
|
|
||||||
<div [ngbNavOutlet]="nav" class="mt-2"></div>
|
<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-secondary" (click)="discard()" i18n [disabled]="networkActive">Discard</button>
|
||||||
<button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()">Save & edit
|
<button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()" i18n [disabled]="networkActive">Save & next</button>
|
||||||
next</button>
|
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||||
<button type="submit" class="btn btn-primary">Save</button>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6 col-xl-8 mb-3">
|
<div class="col-md-6 col-xl-8 mb-3">
|
||||||
<div class="pdf-viewer-container" *ngIf="getContentType() == 'application/pdf'">
|
<div class="pdf-viewer-container" *ngIf="getContentType() == 'application/pdf'">
|
||||||
<pdf-viewer [src]="previewUrl" [original-size]="false" [show-borders]="true" [show-all]="true" [(page)]="previewCurrentPage" (after-load-complete)="pdfPreviewLoaded($event)"></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>
|
</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 { FormControl, FormGroup } from '@angular/forms';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
@@ -16,6 +16,8 @@ import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.
|
|||||||
import { CorrespondentEditDialogComponent } from '../manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-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 { DocumentTypeEditDialogComponent } from '../manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component';
|
||||||
import { PDFDocumentProxy } from 'ng2-pdf-viewer';
|
import { PDFDocumentProxy } from 'ng2-pdf-viewer';
|
||||||
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
|
import { TextComponent } from '../common/input/text/text.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-document-detail',
|
selector: 'app-document-detail',
|
||||||
@@ -24,8 +26,15 @@ import { PDFDocumentProxy } from 'ng2-pdf-viewer';
|
|||||||
})
|
})
|
||||||
export class DocumentDetailComponent implements OnInit {
|
export class DocumentDetailComponent implements OnInit {
|
||||||
|
|
||||||
public expandOriginalMetadata = false;
|
@ViewChild("inputTitle")
|
||||||
public expandArchivedMetadata = false;
|
titleInput: TextComponent
|
||||||
|
|
||||||
|
expandOriginalMetadata = false
|
||||||
|
expandArchivedMetadata = false
|
||||||
|
|
||||||
|
error: any
|
||||||
|
|
||||||
|
networkActive = false
|
||||||
|
|
||||||
documentId: number
|
documentId: number
|
||||||
document: PaperlessDocument
|
document: PaperlessDocument
|
||||||
@@ -60,7 +69,8 @@ export class DocumentDetailComponent implements OnInit {
|
|||||||
private modalService: NgbModal,
|
private modalService: NgbModal,
|
||||||
private openDocumentService: OpenDocumentsService,
|
private openDocumentService: OpenDocumentsService,
|
||||||
private documentListViewService: DocumentListViewService,
|
private documentListViewService: DocumentListViewService,
|
||||||
private documentTitlePipe: DocumentTitlePipe) { }
|
private documentTitlePipe: DocumentTitlePipe,
|
||||||
|
private toastService: ToastService) { }
|
||||||
|
|
||||||
getContentType() {
|
getContentType() {
|
||||||
return this.metadata?.has_archive_version ? 'application/pdf' : this.metadata?.original_mime_type
|
return this.metadata?.has_archive_version ? 'application/pdf' : this.metadata?.original_mime_type
|
||||||
@@ -131,19 +141,34 @@ export class DocumentDetailComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
|
this.networkActive = true
|
||||||
this.documentsService.update(this.document).subscribe(result => {
|
this.documentsService.update(this.document).subscribe(result => {
|
||||||
this.close()
|
this.close()
|
||||||
|
this.networkActive = false
|
||||||
|
this.error = null
|
||||||
|
}, error => {
|
||||||
|
this.networkActive = false
|
||||||
|
this.error = error.error
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
saveEditNext() {
|
saveEditNext() {
|
||||||
|
this.networkActive = true
|
||||||
this.documentsService.update(this.document).subscribe(result => {
|
this.documentsService.update(this.document).subscribe(result => {
|
||||||
|
this.error = null
|
||||||
this.documentListViewService.getNext(this.document.id).subscribe(nextDocId => {
|
this.documentListViewService.getNext(this.document.id).subscribe(nextDocId => {
|
||||||
|
this.networkActive = false
|
||||||
if (nextDocId) {
|
if (nextDocId) {
|
||||||
this.openDocumentService.closeDocument(this.document)
|
this.openDocumentService.closeDocument(this.document)
|
||||||
this.router.navigate(['documents', nextDocId])
|
this.router.navigate(['documents', nextDocId])
|
||||||
|
this.titleInput.focus()
|
||||||
}
|
}
|
||||||
|
}, error => {
|
||||||
|
this.networkActive = false
|
||||||
})
|
})
|
||||||
|
}, error => {
|
||||||
|
this.networkActive = false
|
||||||
|
this.error = error.error
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,15 +183,19 @@ export class DocumentDetailComponent implements OnInit {
|
|||||||
|
|
||||||
delete() {
|
delete() {
|
||||||
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
||||||
modal.componentInstance.title = "Confirm delete"
|
modal.componentInstance.title = $localize`Confirm delete`
|
||||||
modal.componentInstance.messageBold = `Do you really want to delete document '${this.document.title}'?`
|
modal.componentInstance.messageBold = $localize`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.message = $localize`The files for this document will be deleted permanently. This operation cannot be undone.`
|
||||||
modal.componentInstance.btnClass = "btn-danger"
|
modal.componentInstance.btnClass = "btn-danger"
|
||||||
modal.componentInstance.btnCaption = "Delete document"
|
modal.componentInstance.btnCaption = $localize`Delete document`
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
|
modal.componentInstance.buttonsEnabled = false
|
||||||
this.documentsService.delete(this.document).subscribe(() => {
|
this.documentsService.delete(this.document).subscribe(() => {
|
||||||
modal.close()
|
modal.close()
|
||||||
this.close()
|
this.close()
|
||||||
|
}, error => {
|
||||||
|
this.toastService.showError($localize`Error deleting document: ${JSON.stringify(error)}`)
|
||||||
|
modal.componentInstance.buttonsEnabled = true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -16,7 +16,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let m of metadata">
|
<tr *ngFor="let m of metadata">
|
||||||
<td>{{m.prefix}}:{{m.key}}</td>
|
<td>{{m.prefix}}:{{m.key}}</td>
|
||||||
<td>{{m.value}}</td>
|
<td class="metadata-column">{{m.value}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@@ -0,0 +1,3 @@
|
|||||||
|
.metadata-column {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
@@ -15,7 +15,7 @@ export class MetadataCollapseComponent implements OnInit {
|
|||||||
metadata
|
metadata
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
title = "Metadata"
|
title = $localize`Metadata`
|
||||||
|
|
||||||
ngOnInit(): void {
|
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,27 +1,27 @@
|
|||||||
<div class="card mb-3 bg-light shadow-sm" [class.card-selected]="selected" [class.document-card]="selectable">
|
<div class="card mb-3 shadow-sm" [class.card-selected]="selected" [class.document-card]="selectable">
|
||||||
<div class="row no-gutters">
|
<div class="row no-gutters">
|
||||||
<div class="col-md-2 d-none d-lg-block doc-img-background" [class.doc-img-background-selected]="selected">
|
<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" (click)="selected = selectable ? !selected : false">
|
<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 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">
|
<div class="custom-control custom-checkbox">
|
||||||
<input type="checkbox" class="custom-control-input" id="smallCardCheck{{document.id}}" [checked]="selected" (change)="selected = $event.target.checked">
|
<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>
|
<label class="custom-control-label" for="smallCardCheck{{document.id}}"></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card-body">
|
<div class="card-body bg-light">
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<h5 class="card-title">
|
<h5 class="card-title">
|
||||||
<ng-container *ngIf="document.correspondent">
|
<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-template #nolink>{{(document.correspondent$ | async)?.name}}</ng-template>:
|
||||||
</ng-container>
|
</ng-container>
|
||||||
{{document.title | documentTitle}}
|
{{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>
|
||||||
<h5 class="card-title" *ngIf="document.archive_serial_number">#{{document.archive_serial_number}}</h5>
|
<h5 class="card-title" *ngIf="document.archive_serial_number">#{{document.archive_serial_number}}</h5>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,37 +36,33 @@
|
|||||||
<a routerLink="/search" [queryParams]="{'more_like': document.id}" class="btn btn-sm btn-outline-secondary" *ngIf="moreLikeThis">
|
<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">
|
<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"/>
|
<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>
|
</svg> <ng-container i18n>More like this</ng-container>
|
||||||
More like this
|
|
||||||
</a>
|
</a>
|
||||||
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary">
|
<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">
|
<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"/>
|
<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>
|
</svg> <ng-container i18n>Edit</ng-container>
|
||||||
Edit
|
|
||||||
</a>
|
</a>
|
||||||
<a type="button" class="btn btn-sm btn-outline-secondary" [href]="getPreviewUrl()">
|
<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">
|
<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="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"/>
|
<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>
|
</svg> <ng-container i18n>View</ng-container>
|
||||||
View
|
|
||||||
</a>
|
</a>
|
||||||
<a type="button" class="btn btn-sm btn-outline-secondary" [href]="getDownloadUrl()">
|
<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">
|
<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="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"/>
|
<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>
|
</svg> <ng-container i18n>Download</ng-container>
|
||||||
Download
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<small class="text-muted ml-auto">Score:</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>
|
<ngb-progressbar *ngIf="searchScore" [type]="searchScoreClass" [value]="searchScore" class="search-score-bar mx-2" [max]="1"></ngb-progressbar>
|
||||||
|
|
||||||
<small class="text-muted">Created: {{document.created | date}}</small>
|
<small class="text-muted" [class.ml-auto]="!searchScore" i18n>Created: {{document.created | date}}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@@ -30,10 +30,6 @@
|
|||||||
border-color: $primary;
|
border-color: $primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.doc-img-background {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.doc-img-background-selected {
|
.doc-img-background-selected {
|
||||||
background-color: $primaryFaded;
|
background-color: $primaryFaded;
|
||||||
}
|
}
|
@@ -12,15 +12,11 @@ export class DocumentCardLargeComponent implements OnInit {
|
|||||||
|
|
||||||
constructor(private documentService: DocumentService, private sanitizer: DomSanitizer) { }
|
constructor(private documentService: DocumentService, private sanitizer: DomSanitizer) { }
|
||||||
|
|
||||||
_selected = false
|
|
||||||
|
|
||||||
get selected() {
|
|
||||||
return this._selected
|
|
||||||
}
|
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
set selected(value: boolean) {
|
selected = false
|
||||||
this._selected = value
|
|
||||||
|
setSelected(value: boolean) {
|
||||||
|
this.selected = value
|
||||||
this.selectedChange.emit(value)
|
this.selectedChange.emit(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,18 +1,18 @@
|
|||||||
<div class="col p-2 h-100 document-card">
|
<div class="col p-2 h-100">
|
||||||
<div class="card h-100 shadow-sm" [class.card-selected]="selected">
|
<div class="card h-100 shadow-sm document-card" [class.card-selected]="selected">
|
||||||
<div class="border-bottom" [class.doc-img-background-selected]="selected">
|
<div class="border-bottom doc-img-container" [class.doc-img-background-selected]="selected">
|
||||||
<img class="card-img doc-img" [src]="getThumbUrl()" (click)="selected = !selected">
|
<img class="card-img doc-img rounded-top" [src]="getThumbUrl()" (click)="setSelected(!selected)">
|
||||||
|
|
||||||
<div style="top: 0; left: 0" class="position-absolute border-right border-bottom bg-light p-1" [class.document-card-check]="!selected">
|
<div class="border-right border-bottom bg-light p-1 rounded document-card-check">
|
||||||
<div class="custom-control custom-checkbox">
|
<div class="custom-control custom-checkbox">
|
||||||
<input type="checkbox" class="custom-control-input" id="smallCardCheck{{document.id}}" [checked]="selected" (change)="selected = $event.target.checked">
|
<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>
|
<label class="custom-control-label" for="smallCardCheck{{document.id}}"></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="top: 0; right: 0; font-size: large" class="text-right position-absolute mr-1">
|
<div style="top: 0; right: 0; font-size: large" class="text-right position-absolute mr-1">
|
||||||
<div *ngFor="let t of getTagsLimited$() | async">
|
<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>
|
||||||
<div *ngIf="moreTags">
|
<div *ngIf="moreTags">
|
||||||
<span class="badge badge-secondary">+ {{moreTags}}</span>
|
<span class="badge badge-secondary">+ {{moreTags}}</span>
|
||||||
@@ -23,27 +23,27 @@
|
|||||||
<div class="card-body p-2">
|
<div class="card-body p-2">
|
||||||
<p class="card-text">
|
<p class="card-text">
|
||||||
<ng-container *ngIf="document.correspondent">
|
<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>
|
</ng-container>
|
||||||
{{document.title | documentTitle}}
|
{{document.title | documentTitle}} <span *ngIf="document.archive_serial_number">(#{{document.archive_serial_number}})</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mx-n2">
|
<div class="d-flex justify-content-between align-items-center mx-n2">
|
||||||
<div class="btn-group">
|
<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">
|
<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"/>
|
<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>
|
</svg>
|
||||||
</a>
|
</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">
|
<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="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"/>
|
<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>
|
</svg>
|
||||||
</a>
|
</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">
|
<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="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"/>
|
<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"/>
|
||||||
|
@@ -8,7 +8,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.document-card-check {
|
.document-card-check {
|
||||||
display: none
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
.custom-control {
|
||||||
|
margin-left: 4px;
|
||||||
|
margin-right: -3px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.document-card:hover .document-card-check {
|
.document-card:hover .document-card-check {
|
||||||
@@ -17,6 +25,10 @@
|
|||||||
|
|
||||||
.card-selected {
|
.card-selected {
|
||||||
border-color: $primary;
|
border-color: $primary;
|
||||||
|
|
||||||
|
.document-card-check {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.doc-img-background-selected {
|
.doc-img-background-selected {
|
||||||
|
@@ -12,15 +12,11 @@ export class DocumentCardSmallComponent implements OnInit {
|
|||||||
|
|
||||||
constructor(private documentService: DocumentService) { }
|
constructor(private documentService: DocumentService) { }
|
||||||
|
|
||||||
_selected = false
|
|
||||||
|
|
||||||
get selected() {
|
|
||||||
return this._selected
|
|
||||||
}
|
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
set selected(value: boolean) {
|
selected = false
|
||||||
this._selected = value
|
|
||||||
|
setSelected(value: boolean) {
|
||||||
|
this.selected = value
|
||||||
this.selectedChange.emit(value)
|
this.selectedChange.emit(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,25 +1,16 @@
|
|||||||
<app-page-header [title]="getTitle()">
|
<app-page-header [title]="getTitle()">
|
||||||
|
|
||||||
<div ngbDropdown class="d-inline-block mr-2">
|
<div ngbDropdown class="d-inline-block mr-2">
|
||||||
<button class="btn btn-sm btn-outline-primary" id="dropdownBasic1" ngbDropdownToggle>
|
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
|
||||||
<svg class="toolbaricon" fill="currentColor">
|
<svg class="toolbaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#text-indent-left" />
|
<use xlink:href="assets/bootstrap-icons.svg#text-indent-left" />
|
||||||
</svg>
|
</svg> <ng-container i18n>Select</ng-container>
|
||||||
Bulk edit
|
|
||||||
</button>
|
</button>
|
||||||
<div ngbDropdownMenu aria-labelledby="dropdownBasic1" class="shadow">
|
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
|
||||||
<button ngbDropdownItem (click)="list.selectPage()">Select page</button>
|
<button ngbDropdownItem (click)="list.selectNone()" i18n>Select none</button>
|
||||||
<button ngbDropdownItem (click)="list.selectAll()">Select all</button>
|
<button ngbDropdownItem (click)="list.selectPage()" i18n>Select page</button>
|
||||||
<button ngbDropdownItem (click)="list.selectNone()">Select none</button>
|
<button ngbDropdownItem (click)="list.selectAll()" i18n>Select all</button>
|
||||||
<div class="dropdown-divider"></div>
|
|
||||||
<button ngbDropdownItem [disabled]="list.selected.size == 0" (click)="bulkSetCorrespondent()">Set correspondent</button>
|
|
||||||
<button ngbDropdownItem [disabled]="list.selected.size == 0" (click)="bulkRemoveCorrespondent()">Remove correspondent</button>
|
|
||||||
<button ngbDropdownItem [disabled]="list.selected.size == 0" (click)="bulkSetDocumentType()">Set document type</button>
|
|
||||||
<button ngbDropdownItem [disabled]="list.selected.size == 0" (click)="bulkRemoveDocumentType()">Remove document type</button>
|
|
||||||
<button ngbDropdownItem [disabled]="list.selected.size == 0" (click)="bulkAddTag()">Add tag</button>
|
|
||||||
<button ngbDropdownItem [disabled]="list.selected.size == 0" (click)="bulkRemoveTag()">Remove tag</button>
|
|
||||||
<div class="dropdown-divider"></div>
|
|
||||||
<button ngbDropdownItem [disabled]="list.selected.size == 0" (click)="bulkDelete()">Delete</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -47,7 +38,7 @@
|
|||||||
|
|
||||||
<div class="btn-group btn-group-toggle ml-2" ngbRadioGroup [(ngModel)]="list.sortReverse">
|
<div class="btn-group btn-group-toggle ml-2" ngbRadioGroup [(ngModel)]="list.sortReverse">
|
||||||
<div ngbDropdown class="btn-group">
|
<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">
|
<div ngbDropdownMenu aria-labelledby="dropdownBasic1" class="shadow">
|
||||||
<button *ngFor="let f of getSortFields()" ngbDropdownItem (click)="list.sortField = f.field"
|
<button *ngFor="let f of getSortFields()" ngbDropdownItem (click)="list.sortField = f.field"
|
||||||
[class.active]="list.sortField == f.field">{{f.name}}</button>
|
[class.active]="list.sortField == f.field">{{f.name}}</button>
|
||||||
@@ -70,15 +61,15 @@
|
|||||||
<div class="btn-group ml-2">
|
<div class="btn-group ml-2">
|
||||||
|
|
||||||
<div class="btn-group" ngbDropdown role="group">
|
<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>
|
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
||||||
<ng-container *ngIf="!list.savedViewId">
|
<ng-container *ngIf="!list.savedViewId">
|
||||||
<button ngbDropdownItem *ngFor="let view of savedViewService.allViews" (click)="loadViewConfig(view)">{{view.name}}</button>
|
<button ngbDropdownItem *ngFor="let view of savedViewService.allViews" (click)="loadViewConfig(view)">{{view.name}}</button>
|
||||||
<div class="dropdown-divider" *ngIf="savedViewService.allViews.length > 0"></div>
|
<div class="dropdown-divider" *ngIf="savedViewService.allViews.length > 0"></div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.savedViewId">Save "{{list.savedViewTitle}}"</button>
|
<button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.savedViewId" i18n>Save "{{list.savedViewTitle}}"</button>
|
||||||
<button ngbDropdownItem (click)="saveViewConfigAs()">Save as...</button>
|
<button ngbDropdownItem (click)="saveViewConfigAs()" i18n>Save as...</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -87,11 +78,15 @@
|
|||||||
</app-page-header>
|
</app-page-header>
|
||||||
|
|
||||||
<div class="w-100 mb-2 mb-sm-4">
|
<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>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<p><span *ngIf="list.selected.size > 0">Selected {{list.selected.size}} of </span>{{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"
|
<ngb-pagination [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
|
||||||
[rotate]="true" (pageChange)="list.reload()" aria-label="Default pagination"></ngb-pagination>
|
[rotate]="true" (pageChange)="list.reload()" aria-label="Default pagination"></ngb-pagination>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,12 +99,42 @@
|
|||||||
<table class="table table-sm border shadow-sm" *ngIf="displayMode == 'details'">
|
<table class="table table-sm border shadow-sm" *ngIf="displayMode == 'details'">
|
||||||
<thead>
|
<thead>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th class="d-none d-lg-table-cell">ASN</th>
|
<th class="d-none d-lg-table-cell"
|
||||||
<th class="d-none d-md-table-cell">Correspondent</th>
|
sortable="archive_serial_number"
|
||||||
<th>Title</th>
|
[currentSortField]="list.sortField"
|
||||||
<th class="d-none d-xl-table-cell">Document type</th>
|
[currentSortReverse]="list.sortReverse"
|
||||||
<th>Created</th>
|
(sort)="onSort($event)"
|
||||||
<th class="d-none d-xl-table-cell">Added</th>
|
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>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let d of list.documents; trackBy: trackByDocumentId" [ngClass]="list.isSelected(d) ? 'table-row-selected' : ''">
|
<tr *ngFor="let d of list.documents; trackBy: trackByDocumentId" [ngClass]="list.isSelected(d) ? 'table-row-selected' : ''">
|
||||||
@@ -146,7 +171,6 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
||||||
<div class="m-n2 row row-cols-paperless-cards" *ngIf="displayMode == 'smallCards'">
|
<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>
|
<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>
|
</div>
|
||||||
|
@@ -1,22 +1,15 @@
|
|||||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
import { AfterViewInit, Component, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { tap } from 'rxjs/operators';
|
|
||||||
import { PaperlessDocument } from 'src/app/data/paperless-document';
|
import { PaperlessDocument } from 'src/app/data/paperless-document';
|
||||||
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
|
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 { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
||||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
|
import { DOCUMENT_SORT_FIELDS } from 'src/app/services/rest/document.service';
|
||||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service';
|
|
||||||
import { DocumentService, DOCUMENT_SORT_FIELDS } from 'src/app/services/rest/document.service';
|
|
||||||
import { TagService } from 'src/app/services/rest/tag.service';
|
|
||||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
|
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
|
||||||
import { Toast, ToastService } from 'src/app/services/toast.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 { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component';
|
|
||||||
import { SelectDialogComponent } from '../common/select-dialog/select-dialog.component';
|
|
||||||
import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component';
|
import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component';
|
||||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-document-list',
|
selector: 'app-document-list',
|
||||||
@@ -31,30 +24,37 @@ export class DocumentListComponent implements OnInit {
|
|||||||
public route: ActivatedRoute,
|
public route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
public modalService: NgbModal,
|
private modalService: NgbModal) { }
|
||||||
private correspondentService: CorrespondentService,
|
|
||||||
private documentTypeService: DocumentTypeService,
|
|
||||||
private tagService: TagService,
|
|
||||||
private documentService: DocumentService,
|
|
||||||
private openDocumentService: OpenDocumentsService) { }
|
|
||||||
|
|
||||||
@ViewChild("filterEditor")
|
@ViewChild("filterEditor")
|
||||||
private filterEditor: FilterEditorComponent
|
private filterEditor: FilterEditorComponent
|
||||||
|
|
||||||
|
@ViewChildren(SortableDirective) headers: QueryList<SortableDirective>;
|
||||||
|
|
||||||
displayMode = 'smallCards' // largeCards, smallCards, details
|
displayMode = 'smallCards' // largeCards, smallCards, details
|
||||||
|
|
||||||
|
filterRulesModified: boolean = false
|
||||||
|
|
||||||
get isFiltered() {
|
get isFiltered() {
|
||||||
return this.list.filterRules?.length > 0
|
return this.list.filterRules?.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
getTitle() {
|
getTitle() {
|
||||||
return this.list.savedViewTitle || "Documents"
|
return this.list.savedViewTitle || $localize`Documents`
|
||||||
}
|
}
|
||||||
|
|
||||||
getSortFields() {
|
getSortFields() {
|
||||||
return DOCUMENT_SORT_FIELDS
|
return DOCUMENT_SORT_FIELDS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onSort(event: SortEvent) {
|
||||||
|
this.list.setSort(event.column, event.reverse)
|
||||||
|
}
|
||||||
|
|
||||||
|
get isBulkEditing(): boolean {
|
||||||
|
return this.list.selected.size > 0
|
||||||
|
}
|
||||||
|
|
||||||
saveDisplayMode() {
|
saveDisplayMode() {
|
||||||
localStorage.setItem('document-list:displayMode', this.displayMode)
|
localStorage.setItem('document-list:displayMode', this.displayMode)
|
||||||
}
|
}
|
||||||
@@ -71,26 +71,27 @@ export class DocumentListComponent implements OnInit {
|
|||||||
this.router.navigate(["404"])
|
this.router.navigate(["404"])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.list.savedView = view
|
this.list.savedView = view
|
||||||
this.list.reload()
|
this.list.reload()
|
||||||
|
this.rulesChanged()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.list.savedView = null
|
this.list.savedView = null
|
||||||
this.list.reload()
|
this.list.reload()
|
||||||
|
this.rulesChanged()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
loadViewConfig(view: PaperlessSavedView) {
|
loadViewConfig(view: PaperlessSavedView) {
|
||||||
this.list.load(view)
|
this.list.load(view)
|
||||||
this.list.reload()
|
this.list.reload()
|
||||||
|
this.rulesChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
saveViewConfig() {
|
saveViewConfig() {
|
||||||
this.savedViewService.update(this.list.savedView).subscribe(result => {
|
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.`)
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -99,6 +100,7 @@ export class DocumentListComponent implements OnInit {
|
|||||||
let modal = this.modalService.open(SaveViewConfigDialogComponent, {backdrop: 'static'})
|
let modal = this.modalService.open(SaveViewConfigDialogComponent, {backdrop: 'static'})
|
||||||
modal.componentInstance.defaultName = this.filterEditor.generateFilterName()
|
modal.componentInstance.defaultName = this.filterEditor.generateFilterName()
|
||||||
modal.componentInstance.saveClicked.subscribe(formValue => {
|
modal.componentInstance.saveClicked.subscribe(formValue => {
|
||||||
|
modal.componentInstance.buttonsEnabled = false
|
||||||
let savedView = {
|
let savedView = {
|
||||||
name: formValue.name,
|
name: formValue.name,
|
||||||
show_on_dashboard: formValue.showOnDashboard,
|
show_on_dashboard: formValue.showOnDashboard,
|
||||||
@@ -107,141 +109,79 @@ export class DocumentListComponent implements OnInit {
|
|||||||
sort_reverse: this.list.sortReverse,
|
sort_reverse: this.list.sortReverse,
|
||||||
sort_field: this.list.sortField
|
sort_field: this.list.sortField
|
||||||
}
|
}
|
||||||
|
|
||||||
this.savedViewService.create(savedView).subscribe(() => {
|
this.savedViewService.create(savedView).subscribe(() => {
|
||||||
modal.close()
|
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) {
|
clickTag(tagID: number) {
|
||||||
|
this.list.selectNone()
|
||||||
|
setTimeout(() => {
|
||||||
this.filterEditor.toggleTag(tagID)
|
this.filterEditor.toggleTag(tagID)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
clickCorrespondent(correspondentID: number) {
|
clickCorrespondent(correspondentID: number) {
|
||||||
|
this.list.selectNone()
|
||||||
|
setTimeout(() => {
|
||||||
this.filterEditor.toggleCorrespondent(correspondentID)
|
this.filterEditor.toggleCorrespondent(correspondentID)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
clickDocumentType(documentTypeID: number) {
|
clickDocumentType(documentTypeID: number) {
|
||||||
|
this.list.selectNone()
|
||||||
|
setTimeout(() => {
|
||||||
this.filterEditor.toggleDocumentType(documentTypeID)
|
this.filterEditor.toggleDocumentType(documentTypeID)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
trackByDocumentId(index, item: PaperlessDocument) {
|
trackByDocumentId(index, item: PaperlessDocument) {
|
||||||
return item.id
|
return item.id
|
||||||
}
|
}
|
||||||
|
|
||||||
private executeBulkOperation(method: string, args): Observable<any> {
|
|
||||||
return this.documentService.bulkEdit(Array.from(this.list.selected), method, args).pipe(
|
|
||||||
tap(() => {
|
|
||||||
this.list.reload()
|
|
||||||
this.list.selected.forEach(id => {
|
|
||||||
this.openDocumentService.refreshDocument(id)
|
|
||||||
})
|
|
||||||
this.list.selectNone()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
bulkSetCorrespondent() {
|
|
||||||
let modal = this.modalService.open(SelectDialogComponent, {backdrop: 'static'})
|
|
||||||
modal.componentInstance.title = "Select correspondent"
|
|
||||||
modal.componentInstance.message = `Select the correspondent you wish to assign to ${this.list.selected.size} selected document(s):`
|
|
||||||
this.correspondentService.listAll().subscribe(response => {
|
|
||||||
modal.componentInstance.objects = response.results
|
|
||||||
})
|
|
||||||
modal.componentInstance.selectClicked.subscribe(selectedId => {
|
|
||||||
this.executeBulkOperation('set_correspondent', {"correspondent": selectedId}).subscribe(
|
|
||||||
response => {
|
|
||||||
modal.close()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
bulkRemoveCorrespondent() {
|
|
||||||
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
|
||||||
modal.componentInstance.title = "Remove correspondent"
|
|
||||||
modal.componentInstance.message = `This operation will remove the correspondent from all ${this.list.selected.size} selected document(s).`
|
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
|
||||||
this.executeBulkOperation('set_correspondent', {"correspondent": null}).subscribe(r => {
|
|
||||||
modal.close()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
bulkSetDocumentType() {
|
|
||||||
let modal = this.modalService.open(SelectDialogComponent, {backdrop: 'static'})
|
|
||||||
modal.componentInstance.title = "Select document type"
|
|
||||||
modal.componentInstance.message = `Select the document type you wish to assign to ${this.list.selected.size} selected document(s):`
|
|
||||||
this.documentTypeService.listAll().subscribe(response => {
|
|
||||||
modal.componentInstance.objects = response.results
|
|
||||||
})
|
|
||||||
modal.componentInstance.selectClicked.subscribe(selectedId => {
|
|
||||||
this.executeBulkOperation('set_document_type', {"document_type": selectedId}).subscribe(
|
|
||||||
response => {
|
|
||||||
modal.close()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
bulkRemoveDocumentType() {
|
|
||||||
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
|
||||||
modal.componentInstance.title = "Remove document type"
|
|
||||||
modal.componentInstance.message = `This operation will remove the document type from all ${this.list.selected.size} selected document(s).`
|
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
|
||||||
this.executeBulkOperation('set_document_type', {"document_type": null}).subscribe(r => {
|
|
||||||
modal.close()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
bulkAddTag() {
|
|
||||||
let modal = this.modalService.open(SelectDialogComponent, {backdrop: 'static'})
|
|
||||||
modal.componentInstance.title = "Select tag"
|
|
||||||
modal.componentInstance.message = `Select the tag you wish to assign to ${this.list.selected.size} selected document(s):`
|
|
||||||
this.tagService.listAll().subscribe(response => {
|
|
||||||
modal.componentInstance.objects = response.results
|
|
||||||
})
|
|
||||||
modal.componentInstance.selectClicked.subscribe(selectedId => {
|
|
||||||
this.executeBulkOperation('add_tag', {"tag": selectedId}).subscribe(
|
|
||||||
response => {
|
|
||||||
modal.close()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
bulkRemoveTag() {
|
|
||||||
let modal = this.modalService.open(SelectDialogComponent, {backdrop: 'static'})
|
|
||||||
modal.componentInstance.title = "Select tag"
|
|
||||||
modal.componentInstance.message = `Select the tag you wish to remove from ${this.list.selected.size} selected document(s):`
|
|
||||||
this.tagService.listAll().subscribe(response => {
|
|
||||||
modal.componentInstance.objects = response.results
|
|
||||||
})
|
|
||||||
modal.componentInstance.selectClicked.subscribe(selectedId => {
|
|
||||||
this.executeBulkOperation('remove_tag', {"tag": selectedId}).subscribe(
|
|
||||||
response => {
|
|
||||||
modal.close()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
bulkDelete() {
|
|
||||||
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
|
||||||
modal.componentInstance.delayConfirm(5)
|
|
||||||
modal.componentInstance.title = "Delete confirm"
|
|
||||||
modal.componentInstance.messageBold = `This operation will permanently delete all ${this.list.selected.size} selected document(s).`
|
|
||||||
modal.componentInstance.message = `This operation cannot be undone.`
|
|
||||||
modal.componentInstance.btnClass = "btn-danger"
|
|
||||||
modal.componentInstance.btnCaption = "Delete document(s)"
|
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
|
||||||
this.executeBulkOperation("delete", {}).subscribe(
|
|
||||||
response => {
|
|
||||||
modal.close()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -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">
|
<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()">
|
<button type="button" class="close" aria-label="Close" (click)="cancel()">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<app-input-text title="Name" formControlName="name"></app-input-text>
|
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
||||||
<app-input-check title="Show in side bar" formControlName="showInSideBar"></app-input-check>
|
<app-input-check i18n-title title="Show in sidebar" formControlName="showInSideBar"></app-input-check>
|
||||||
<app-input-check title="Show on dashboard" formControlName="showOnDashboard"></app-input-check>
|
<app-input-check i18n-title title="Show on dashboard" formControlName="showOnDashboard"></app-input-check>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button>
|
<button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n [disabled]="!buttonsEnabled">Cancel</button>
|
||||||
<button type="submit" class="btn btn-primary">Save</button>
|
<button type="submit" class="btn btn-primary" i18n [disabled]="!buttonsEnabled">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@@ -14,6 +14,12 @@ export class SaveViewConfigDialogComponent implements OnInit {
|
|||||||
@Output()
|
@Output()
|
||||||
public saveClicked = new EventEmitter()
|
public saveClicked = new EventEmitter()
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
error
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
buttonsEnabled = true
|
||||||
|
|
||||||
_defaultName = ""
|
_defaultName = ""
|
||||||
|
|
||||||
get defaultName() {
|
get defaultName() {
|
||||||
@@ -26,7 +32,6 @@ export class SaveViewConfigDialogComponent implements OnInit {
|
|||||||
this.saveViewConfigForm.patchValue({name: value})
|
this.saveViewConfigForm.patchValue({name: value})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
saveViewConfigForm = new FormGroup({
|
saveViewConfigForm = new FormGroup({
|
||||||
name: new FormControl(''),
|
name: new FormControl(''),
|
||||||
showInSideBar: new FormControl(false),
|
showInSideBar: new FormControl(false),
|
||||||
|
@@ -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>
|
|
@@ -1,58 +0,0 @@
|
|||||||
import { Component, EventEmitter, Input, Output, ElementRef, ViewChild } from '@angular/core';
|
|
||||||
import { ObjectWithId } from 'src/app/data/object-with-id';
|
|
||||||
import { FilterPipe } from 'src/app/pipes/filter.pipe';
|
|
||||||
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-filter-dropdown',
|
|
||||||
templateUrl: './filter-dropdown.component.html',
|
|
||||||
styleUrls: ['./filter-dropdown.component.scss']
|
|
||||||
})
|
|
||||||
export class FilterDropdownComponent {
|
|
||||||
|
|
||||||
constructor(private filterPipe: FilterPipe) { }
|
|
||||||
|
|
||||||
@Input()
|
|
||||||
items: ObjectWithId[]
|
|
||||||
|
|
||||||
@Input()
|
|
||||||
itemsSelected: ObjectWithId[]
|
|
||||||
|
|
||||||
@Input()
|
|
||||||
title: string
|
|
||||||
|
|
||||||
@Input()
|
|
||||||
icon: string
|
|
||||||
|
|
||||||
@Output()
|
|
||||||
toggle = new EventEmitter()
|
|
||||||
|
|
||||||
@ViewChild('listFilterTextInput') listFilterTextInput: ElementRef
|
|
||||||
@ViewChild('filterDropdown') filterDropdown: NgbDropdown
|
|
||||||
|
|
||||||
filterText: string
|
|
||||||
|
|
||||||
toggleItem(item: ObjectWithId): void {
|
|
||||||
this.toggle.emit(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
isItemSelected(item: ObjectWithId): boolean {
|
|
||||||
return this.itemsSelected?.find(i => i.id == item.id) !== undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
dropdownOpenChange(open: boolean): void {
|
|
||||||
if (open) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.listFilterTextInput.nativeElement.focus();
|
|
||||||
}, 0);
|
|
||||||
} else {
|
|
||||||
this.filterText = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
listFilterEnter(): void {
|
|
||||||
let filtered = this.filterPipe.transform(this.items, this.filterText)
|
|
||||||
if (filtered.length == 1) this.toggleItem(filtered.shift())
|
|
||||||
this.filterDropdown.close()
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,27 +0,0 @@
|
|||||||
<div class="row">
|
|
||||||
<div class="col mb-2 mb-xl-0">
|
|
||||||
<div class="form-inline d-flex">
|
|
||||||
<label class="text-muted mr-2">Filter by:</label>
|
|
||||||
<input class="form-control form-control-sm flex-grow-1" type="text" [(ngModel)]="titleFilter" placeholder="Title">
|
|
||||||
</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-filter-dropdown class="mr-2 mr-md-3" [items]="tags" [itemsSelected]="selectedTags" title="Tags" icon="tag-fill" (toggle)="toggleTag($event.id)"></app-filter-dropdown>
|
|
||||||
<app-filter-dropdown class="mr-2 mr-md-3" [items]="correspondents" [itemsSelected]="selectedCorrespondents" title="Correspondents" icon="person-fill" (toggle)="toggleCorrespondent($event.id)"></app-filter-dropdown>
|
|
||||||
<app-filter-dropdown class="mr-2 mr-md-3" [items]="documentTypes" [itemsSelected]="selectedDocumentTypes" title="Document types" icon="file-earmark-fill" (toggle)="toggleDocumentType($event.id)"></app-filter-dropdown>
|
|
||||||
<app-filter-dropdown-date class="mr-2 mr-md-3" [dateBefore]="dateCreatedBefore" [dateAfter]="dateCreatedAfter" title="Created" (datesSet)="onDatesCreatedSet($event)"></app-filter-dropdown-date>
|
|
||||||
<app-filter-dropdown-date [dateBefore]="dateAddedBefore" [dateAfter]="dateAddedAfter" title="Added" (datesSet)="onDatesAddedSet($event)"></app-filter-dropdown-date>
|
|
||||||
</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]="!hasFilters()" (click)="clearSelected()">
|
|
||||||
<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>
|
|
||||||
Clear all filters
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@@ -1,239 +0,0 @@
|
|||||||
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 { NgbDateParserFormatter, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap';
|
|
||||||
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_TAG, FILTER_RULE_TYPES, FILTER_TITLE } from 'src/app/data/filter-rule-type';
|
|
||||||
import { DateSelection } from './filter-dropdown-date/filter-dropdown-date.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:
|
|
||||||
return `Correspondent: ${this.correspondents.find(c => c.id == +rule.value)?.name}`
|
|
||||||
|
|
||||||
case FILTER_DOCUMENT_TYPE:
|
|
||||||
return `Type: ${this.documentTypes.find(dt => dt.id == +rule.value)?.name}`
|
|
||||||
|
|
||||||
case FILTER_HAS_TAG:
|
|
||||||
return `Tag: ${this.tags.find(t => t.id == +rule.value)?.name}`
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private documentTypeService: DocumentTypeService,
|
|
||||||
private tagService: TagService,
|
|
||||||
private correspondentService: CorrespondentService,
|
|
||||||
private dateParser: NgbDateParserFormatter
|
|
||||||
) { }
|
|
||||||
|
|
||||||
tags: PaperlessTag[] = []
|
|
||||||
correspondents: PaperlessCorrespondent[]
|
|
||||||
documentTypes: PaperlessDocumentType[] = []
|
|
||||||
|
|
||||||
@Input()
|
|
||||||
filterRules: FilterRule[]
|
|
||||||
|
|
||||||
@Output()
|
|
||||||
filterRulesChange = new EventEmitter<FilterRule[]>()
|
|
||||||
|
|
||||||
hasFilters() {
|
|
||||||
return this.filterRules.length > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
get selectedTags(): PaperlessTag[] {
|
|
||||||
let tagRules: FilterRule[] = this.filterRules.filter(fr => fr.rule_type == FILTER_HAS_TAG)
|
|
||||||
return this.tags?.filter(t => tagRules.find(tr => +tr.value == t.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
get selectedCorrespondents(): PaperlessCorrespondent[] {
|
|
||||||
let correspondentRules: FilterRule[] = this.filterRules.filter(fr => fr.rule_type == FILTER_CORRESPONDENT)
|
|
||||||
return this.correspondents?.filter(c => correspondentRules.find(cr => +cr.value == c.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
get selectedDocumentTypes(): PaperlessDocumentType[] {
|
|
||||||
let documentTypeRules: FilterRule[] = this.filterRules.filter(fr => fr.rule_type == FILTER_DOCUMENT_TYPE)
|
|
||||||
return this.documentTypes?.filter(dt => documentTypeRules.find(dtr => +dtr.value == dt.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
get titleFilter() {
|
|
||||||
let existingRule = this.filterRules.find(rule => rule.rule_type == FILTER_TITLE)
|
|
||||||
return existingRule ? existingRule.value : ''
|
|
||||||
}
|
|
||||||
|
|
||||||
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.setTitleRule(title)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
this.titleFilterDebounce.complete()
|
|
||||||
// TODO: not sure if both is necessary
|
|
||||||
this.subscription.unsubscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
applyFilters() {
|
|
||||||
this.filterRulesChange.next(this.filterRules)
|
|
||||||
}
|
|
||||||
|
|
||||||
clearSelected() {
|
|
||||||
this.filterRules = []
|
|
||||||
this.applyFilters()
|
|
||||||
}
|
|
||||||
|
|
||||||
private toggleFilterRule(filterRuleTypeID: number, value: number) {
|
|
||||||
|
|
||||||
let filterRuleType = FILTER_RULE_TYPES.find(t => t.id == filterRuleTypeID)
|
|
||||||
|
|
||||||
let existingRule = this.filterRules.find(rule => rule.rule_type == filterRuleTypeID && rule.value == value?.toString())
|
|
||||||
let existingRuleOfSameType = this.filterRules.find(rule => rule.rule_type == filterRuleTypeID)
|
|
||||||
|
|
||||||
if (existingRule) {
|
|
||||||
// if this exact rule already exists, remove it in all cases.
|
|
||||||
this.filterRules.splice(this.filterRules.indexOf(existingRule), 1)
|
|
||||||
} else if (filterRuleType.multi || !existingRuleOfSameType) {
|
|
||||||
// if we allow multiple rules per type, or no rule of this type already exists, push a new rule.
|
|
||||||
this.filterRules.push({rule_type: filterRuleTypeID, value: value?.toString()})
|
|
||||||
} else {
|
|
||||||
// otherwise (i.e., no multi support AND there's already a rule of this type), update the rule.
|
|
||||||
existingRuleOfSameType.value = value?.toString()
|
|
||||||
}
|
|
||||||
this.applyFilters()
|
|
||||||
}
|
|
||||||
|
|
||||||
private setTitleRule(title: string) {
|
|
||||||
let existingRule = this.filterRules.find(rule => rule.rule_type == FILTER_TITLE)
|
|
||||||
|
|
||||||
if (!existingRule && title) {
|
|
||||||
this.filterRules.push({rule_type: FILTER_TITLE, value: title})
|
|
||||||
} else if (existingRule && !title) {
|
|
||||||
this.filterRules.splice(this.filterRules.findIndex(rule => rule.rule_type == FILTER_TITLE), 1)
|
|
||||||
} else if (existingRule && title) {
|
|
||||||
existingRule.value = title
|
|
||||||
}
|
|
||||||
this.applyFilters()
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleTag(tagId: number) {
|
|
||||||
this.toggleFilterRule(FILTER_HAS_TAG, tagId)
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleCorrespondent(correspondentId: number) {
|
|
||||||
this.toggleFilterRule(FILTER_CORRESPONDENT, correspondentId)
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleDocumentType(documentTypeId: number) {
|
|
||||||
this.toggleFilterRule(FILTER_DOCUMENT_TYPE, documentTypeId)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Date handling
|
|
||||||
|
|
||||||
|
|
||||||
onDatesCreatedSet(dates: DateSelection) {
|
|
||||||
this.setDateCreatedBefore(dates.before)
|
|
||||||
this.setDateCreatedAfter(dates.after)
|
|
||||||
this.applyFilters()
|
|
||||||
}
|
|
||||||
|
|
||||||
onDatesAddedSet(dates: DateSelection) {
|
|
||||||
this.setDateAddedBefore(dates.before)
|
|
||||||
this.setDateAddedAfter(dates.after)
|
|
||||||
this.applyFilters()
|
|
||||||
}
|
|
||||||
|
|
||||||
get dateCreatedBefore(): string {
|
|
||||||
let createdBeforeRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_CREATED_BEFORE)
|
|
||||||
return createdBeforeRule ? createdBeforeRule.value : null
|
|
||||||
}
|
|
||||||
|
|
||||||
get dateCreatedAfter(): string {
|
|
||||||
let createdAfterRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_CREATED_AFTER)
|
|
||||||
return createdAfterRule ? createdAfterRule.value : null
|
|
||||||
}
|
|
||||||
|
|
||||||
get dateAddedBefore(): string {
|
|
||||||
let addedBeforeRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_ADDED_BEFORE)
|
|
||||||
return addedBeforeRule ? addedBeforeRule.value : null
|
|
||||||
}
|
|
||||||
|
|
||||||
get dateAddedAfter(): string {
|
|
||||||
let addedAfterRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_ADDED_AFTER)
|
|
||||||
return addedAfterRule ? addedAfterRule.value : null
|
|
||||||
}
|
|
||||||
|
|
||||||
setDateCreatedBefore(date?: string) {
|
|
||||||
if (date) this.setDateFilter(date, FILTER_CREATED_BEFORE)
|
|
||||||
else this.clearDateFilter(FILTER_CREATED_BEFORE)
|
|
||||||
}
|
|
||||||
|
|
||||||
setDateCreatedAfter(date?: string) {
|
|
||||||
if (date) this.setDateFilter(date, FILTER_CREATED_AFTER)
|
|
||||||
else this.clearDateFilter(FILTER_CREATED_AFTER)
|
|
||||||
}
|
|
||||||
|
|
||||||
setDateAddedBefore(date?: string) {
|
|
||||||
if (date) this.setDateFilter(date, FILTER_ADDED_BEFORE)
|
|
||||||
else this.clearDateFilter(FILTER_ADDED_BEFORE)
|
|
||||||
}
|
|
||||||
|
|
||||||
setDateAddedAfter(date?: string) {
|
|
||||||
if (date) this.setDateFilter(date, FILTER_ADDED_AFTER)
|
|
||||||
else this.clearDateFilter(FILTER_ADDED_AFTER)
|
|
||||||
}
|
|
||||||
|
|
||||||
setDateFilter(date: string, dateRuleTypeID: number) {
|
|
||||||
let existingRule = this.filterRules.find(rule => rule.rule_type == dateRuleTypeID)
|
|
||||||
|
|
||||||
if (existingRule) {
|
|
||||||
existingRule.value = date
|
|
||||||
} else {
|
|
||||||
this.filterRules.push({rule_type: dateRuleTypeID, value: date})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clearDateFilter(dateRuleTypeID: number) {
|
|
||||||
let ruleIndex = this.filterRules.findIndex(rule => rule.rule_type == dateRuleTypeID)
|
|
||||||
if (ruleIndex != -1) {
|
|
||||||
this.filterRules.splice(ruleIndex, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,4 +1,4 @@
|
|||||||
<form [formGroup]="objectForm" class="needs-validation" novalidate (ngSubmit)="save()">
|
<form [formGroup]="objectForm" (ngSubmit)="save()">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||||
<button type="button" class="close" aria-label="Close" (click)="cancel()">
|
<button type="button" class="close" aria-label="Close" (click)="cancel()">
|
||||||
@@ -7,13 +7,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|
||||||
<app-input-text title="Name" formControlName="name"></app-input-text>
|
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
||||||
<app-input-select title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
||||||
<app-input-text title="Match" formControlName="match" hint="Auto matching does not require you to fill in this field."></app-input-text>
|
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match"></app-input-text>
|
||||||
<app-input-check title="Case insensitive" formControlName="is_insensitive" hint="Auto matching ignores this option."></app-input-check>
|
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive" novalidate></app-input-check>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button>
|
<button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||||
<button type="submit" class="btn btn-primary">Save</button>
|
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
@@ -14,7 +14,15 @@ import { ToastService } from 'src/app/services/toast.service';
|
|||||||
export class CorrespondentEditDialogComponent extends EditDialogComponent<PaperlessCorrespondent> {
|
export class CorrespondentEditDialogComponent extends EditDialogComponent<PaperlessCorrespondent> {
|
||||||
|
|
||||||
constructor(service: CorrespondentService, activeModal: NgbActiveModal, toastService: ToastService) {
|
constructor(service: CorrespondentService, activeModal: NgbActiveModal, toastService: ToastService) {
|
||||||
super(service, activeModal, toastService, 'correspondent')
|
super(service, activeModal, toastService)
|
||||||
|
}
|
||||||
|
|
||||||
|
getCreateTitle() {
|
||||||
|
return $localize`Create new correspondent`
|
||||||
|
}
|
||||||
|
|
||||||
|
getEditTitle() {
|
||||||
|
return $localize`Edit correspondent`
|
||||||
}
|
}
|
||||||
|
|
||||||
getForm(): FormGroup {
|
getForm(): FormGroup {
|
||||||
|
@@ -1,7 +1,5 @@
|
|||||||
<app-page-header title="Correspondents">
|
<app-page-header title="Correspondents" i18n-title>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()">
|
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" i18n>Create</button>
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
</app-page-header>
|
</app-page-header>
|
||||||
|
|
||||||
<div class="row m-0 justify-content-end">
|
<div class="row m-0 justify-content-end">
|
||||||
@@ -11,11 +9,11 @@
|
|||||||
<table class="table table-striped border shadow">
|
<table class="table table-striped border shadow">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" sortable="name" (sort)="onSort($event)">Name</th>
|
<th scope="col" sortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th>
|
||||||
<th scope="col" sortable="matching_algorithm" (sort)="onSort($event)">Matching</th>
|
<th scope="col" sortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th>
|
||||||
<th scope="col" sortable="document_count" (sort)="onSort($event)">Document count</th>
|
<th scope="col" sortable="document_count" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Document count</th>
|
||||||
<th scope="col" sortable="last_correspondence" (sort)="onSort($event)">Last correspondence</th>
|
<th scope="col" sortable="last_correspondence" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Last correspondence</th>
|
||||||
<th scope="col">Actions</th>
|
<th scope="col" i18n>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -29,21 +27,18 @@
|
|||||||
<button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(correspondent)">
|
<button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(correspondent)">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-funnel" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-funnel" viewBox="0 0 16 16">
|
||||||
<path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2h-11z"/>
|
<path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2h-11z"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>Documents</ng-container>
|
||||||
Documents
|
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(correspondent)">
|
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(correspondent)">
|
||||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
<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"/>
|
<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>
|
</svg> <ng-container i18n>Edit</ng-container>
|
||||||
Edit
|
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(correspondent)">
|
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(correspondent)">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
||||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
||||||
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>Delete</ng-container>
|
||||||
Delete
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { FILTER_CORRESPONDENT } from 'src/app/data/filter-rule-type';
|
import { FILTER_CORRESPONDENT } from 'src/app/data/filter-rule-type';
|
||||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
|
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
|
||||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
||||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
|
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
|
||||||
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
import { GenericListComponent } from '../generic-list/generic-list.component';
|
import { GenericListComponent } from '../generic-list/generic-list.component';
|
||||||
import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog/correspondent-edit-dialog.component';
|
import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog/correspondent-edit-dialog.component';
|
||||||
|
|
||||||
@@ -16,20 +16,17 @@ import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog/co
|
|||||||
export class CorrespondentListComponent extends GenericListComponent<PaperlessCorrespondent> {
|
export class CorrespondentListComponent extends GenericListComponent<PaperlessCorrespondent> {
|
||||||
|
|
||||||
constructor(correspondentsService: CorrespondentService, modalService: NgbModal,
|
constructor(correspondentsService: CorrespondentService, modalService: NgbModal,
|
||||||
private router: Router,
|
private list: DocumentListViewService,
|
||||||
private list: DocumentListViewService
|
toastService: ToastService
|
||||||
) {
|
) {
|
||||||
super(correspondentsService,modalService,CorrespondentEditDialogComponent)
|
super(correspondentsService,modalService,CorrespondentEditDialogComponent, toastService)
|
||||||
}
|
}
|
||||||
|
|
||||||
getObjectName(object: PaperlessCorrespondent) {
|
getDeleteMessage(object: PaperlessCorrespondent) {
|
||||||
return `correspondent '${object.name}'`
|
return $localize`Do you really want to delete the correspondent "${object.name}"?`
|
||||||
}
|
}
|
||||||
|
|
||||||
filterDocuments(object: PaperlessCorrespondent) {
|
filterDocuments(object: PaperlessCorrespondent) {
|
||||||
this.list.documentListView.filter_rules = [
|
this.list.quickFilter([{rule_type: FILTER_CORRESPONDENT, value: object.id.toString()}])
|
||||||
{rule_type: FILTER_CORRESPONDENT, value: object.id.toString()}
|
|
||||||
]
|
|
||||||
this.router.navigate(["documents"])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<form [formGroup]="objectForm" class="needs-validation" novalidate (ngSubmit)="save()">
|
<form [formGroup]="objectForm" (ngSubmit)="save()">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||||
<button type="button" class="close" aria-label="Close" (click)="cancel()">
|
<button type="button" class="close" aria-label="Close" (click)="cancel()">
|
||||||
@@ -7,14 +7,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|
||||||
<app-input-text title="Name" formControlName="name"></app-input-text>
|
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
||||||
<app-input-select title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
||||||
<app-input-text title="Match" formControlName="match" hint="Auto matching does not require you to fill in this field."></app-input-text>
|
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match"></app-input-text>
|
||||||
<app-input-check title="Case insensitive" formControlName="is_insensitive" hint="Auto matching ignores this option."></app-input-check>
|
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button>
|
<button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||||
<button type="submit" class="btn btn-primary">Save</button>
|
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
@@ -14,7 +14,15 @@ import { ToastService } from 'src/app/services/toast.service';
|
|||||||
export class DocumentTypeEditDialogComponent extends EditDialogComponent<PaperlessDocumentType> {
|
export class DocumentTypeEditDialogComponent extends EditDialogComponent<PaperlessDocumentType> {
|
||||||
|
|
||||||
constructor(service: DocumentTypeService, activeModal: NgbActiveModal, toastService: ToastService) {
|
constructor(service: DocumentTypeService, activeModal: NgbActiveModal, toastService: ToastService) {
|
||||||
super(service, activeModal, toastService, 'document type')
|
super(service, activeModal, toastService)
|
||||||
|
}
|
||||||
|
|
||||||
|
getCreateTitle() {
|
||||||
|
return $localize`Create new document type`
|
||||||
|
}
|
||||||
|
|
||||||
|
getEditTitle() {
|
||||||
|
return $localize`Edit document type`
|
||||||
}
|
}
|
||||||
|
|
||||||
getForm(): FormGroup {
|
getForm(): FormGroup {
|
||||||
|
@@ -1,7 +1,5 @@
|
|||||||
<app-page-header title="Document types">
|
<app-page-header title="Document types" i18n-title>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()">
|
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" i18n>Create</button>
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
</app-page-header>
|
</app-page-header>
|
||||||
|
|
||||||
<div class="row m-0 justify-content-end">
|
<div class="row m-0 justify-content-end">
|
||||||
@@ -12,10 +10,10 @@
|
|||||||
<table class="table table-striped border shadow">
|
<table class="table table-striped border shadow">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" sortable="name" (sort)="onSort($event)">Name</th>
|
<th scope="col" sortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th>
|
||||||
<th scope="col" sortable="matching_algorithm" (sort)="onSort($event)">Matching</th>
|
<th scope="col" sortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th>
|
||||||
<th scope="col" sortable="document_count" (sort)="onSort($event)">Document count</th>
|
<th scope="col" sortable="document_count" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Document count</th>
|
||||||
<th scope="col">Actions</th>
|
<th scope="col" i18n>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -28,21 +26,18 @@
|
|||||||
<button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(document_type)">
|
<button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(document_type)">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-funnel" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-funnel" viewBox="0 0 16 16">
|
||||||
<path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2h-11z"/>
|
<path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2h-11z"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>Documents</ng-container>
|
||||||
Documents
|
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(document_type)">
|
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(document_type)">
|
||||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
<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"/>
|
<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>
|
</svg> <ng-container i18n>Edit</ng-container>
|
||||||
Edit
|
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(document_type)">
|
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(document_type)">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
||||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
||||||
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>Delete</ng-container>
|
||||||
Delete
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { FILTER_DOCUMENT_TYPE } from 'src/app/data/filter-rule-type';
|
import { FILTER_DOCUMENT_TYPE } from 'src/app/data/filter-rule-type';
|
||||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
|
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
|
||||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
||||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service';
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service';
|
||||||
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
import { GenericListComponent } from '../generic-list/generic-list.component';
|
import { GenericListComponent } from '../generic-list/generic-list.component';
|
||||||
import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog/document-type-edit-dialog.component';
|
import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog/document-type-edit-dialog.component';
|
||||||
|
|
||||||
@@ -16,20 +16,18 @@ import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog/doc
|
|||||||
export class DocumentTypeListComponent extends GenericListComponent<PaperlessDocumentType> {
|
export class DocumentTypeListComponent extends GenericListComponent<PaperlessDocumentType> {
|
||||||
|
|
||||||
constructor(service: DocumentTypeService, modalService: NgbModal,
|
constructor(service: DocumentTypeService, modalService: NgbModal,
|
||||||
private router: Router,
|
private list: DocumentListViewService,
|
||||||
private list: DocumentListViewService
|
toastService: ToastService
|
||||||
) {
|
) {
|
||||||
super(service, modalService, DocumentTypeEditDialogComponent)
|
super(service, modalService, DocumentTypeEditDialogComponent, toastService)
|
||||||
}
|
}
|
||||||
|
|
||||||
getObjectName(object: PaperlessDocumentType) {
|
getDeleteMessage(object: PaperlessDocumentType) {
|
||||||
return `document type '${object.name}'`
|
return $localize`Do you really want to delete the document type "${object.name}"?`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
filterDocuments(object: PaperlessDocumentType) {
|
filterDocuments(object: PaperlessDocumentType) {
|
||||||
this.list.documentListView.filter_rules = [
|
this.list.quickFilter([{rule_type: FILTER_DOCUMENT_TYPE, value: object.id.toString()}])
|
||||||
{rule_type: FILTER_DOCUMENT_TYPE, value: object.id.toString()}
|
|
||||||
]
|
|
||||||
this.router.navigate(["documents"])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@ import { MatchingModel, MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/mat
|
|||||||
import { ObjectWithId } from 'src/app/data/object-with-id';
|
import { ObjectWithId } from 'src/app/data/object-with-id';
|
||||||
import { SortableDirective, SortEvent } from 'src/app/directives/sortable.directive';
|
import { SortableDirective, SortEvent } from 'src/app/directives/sortable.directive';
|
||||||
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service';
|
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service';
|
||||||
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component';
|
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component';
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
@@ -12,7 +13,8 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On
|
|||||||
constructor(
|
constructor(
|
||||||
private service: AbstractPaperlessService<T>,
|
private service: AbstractPaperlessService<T>,
|
||||||
private modalService: NgbModal,
|
private modalService: NgbModal,
|
||||||
private editDialogComponent: any) {
|
private editDialogComponent: any,
|
||||||
|
private toastService: ToastService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewChildren(SortableDirective) headers: QueryList<SortableDirective>;
|
@ViewChildren(SortableDirective) headers: QueryList<SortableDirective>;
|
||||||
@@ -24,34 +26,21 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On
|
|||||||
public collectionSize = 0
|
public collectionSize = 0
|
||||||
|
|
||||||
public sortField: string
|
public sortField: string
|
||||||
public sortDirection: string
|
public sortReverse: boolean
|
||||||
|
|
||||||
getMatching(o: MatchingModel) {
|
getMatching(o: MatchingModel) {
|
||||||
if (o.matching_algorithm == MATCH_AUTO) {
|
if (o.matching_algorithm == MATCH_AUTO) {
|
||||||
return "Automatic"
|
return $localize`Automatic`
|
||||||
} else if (o.match && o.match.length > 0) {
|
} else if (o.match && o.match.length > 0) {
|
||||||
return `${o.match} (${MATCHING_ALGORITHMS.find(a => a.id == o.matching_algorithm).name})`
|
return `${MATCHING_ALGORITHMS.find(a => a.id == o.matching_algorithm).shortName}: ${o.match}`
|
||||||
} else {
|
} else {
|
||||||
return "-"
|
return "-"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSort(event: SortEvent) {
|
onSort(event: SortEvent) {
|
||||||
|
|
||||||
if (event.direction && event.direction.length > 0) {
|
|
||||||
this.sortField = event.column
|
this.sortField = event.column
|
||||||
this.sortDirection = event.direction
|
this.sortReverse = event.reverse
|
||||||
} else {
|
|
||||||
this.sortField = null
|
|
||||||
this.sortDirection = null
|
|
||||||
}
|
|
||||||
|
|
||||||
this.headers.forEach(header => {
|
|
||||||
if (header.sortable !== this.sortField) {
|
|
||||||
header.direction = '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.reloadData()
|
this.reloadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,8 +49,7 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On
|
|||||||
}
|
}
|
||||||
|
|
||||||
reloadData() {
|
reloadData() {
|
||||||
// TODO: this is a hack
|
this.service.list(this.page, null, this.sortField, this.sortReverse).subscribe(c => {
|
||||||
this.service.list(this.page, null, this.sortField, this.sortDirection == 'des').subscribe(c => {
|
|
||||||
this.data = c.results
|
this.data = c.results
|
||||||
this.collectionSize = c.count
|
this.collectionSize = c.count
|
||||||
});
|
});
|
||||||
@@ -84,21 +72,25 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getObjectName(object: T) {
|
getDeleteMessage(object: T) {
|
||||||
return object.toString()
|
return $localize`Do you really want to delete this element?`
|
||||||
}
|
}
|
||||||
|
|
||||||
openDeleteDialog(object: T) {
|
openDeleteDialog(object: T) {
|
||||||
var activeModal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
var activeModal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
||||||
activeModal.componentInstance.title = "Confirm delete"
|
activeModal.componentInstance.title = $localize`Confirm delete`
|
||||||
activeModal.componentInstance.messageBold = `Do you really want to delete ${this.getObjectName(object)}?`
|
activeModal.componentInstance.messageBold = this.getDeleteMessage(object)
|
||||||
activeModal.componentInstance.message = "Associated documents will not be deleted."
|
activeModal.componentInstance.message = $localize`Associated documents will not be deleted.`
|
||||||
activeModal.componentInstance.btnClass = "btn-danger"
|
activeModal.componentInstance.btnClass = "btn-danger"
|
||||||
activeModal.componentInstance.btnCaption = "Delete"
|
activeModal.componentInstance.btnCaption = $localize`Delete`
|
||||||
activeModal.componentInstance.confirmClicked.subscribe(() => {
|
activeModal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
|
activeModal.componentInstance.buttonsEnabled = false
|
||||||
this.service.delete(object).subscribe(_ => {
|
this.service.delete(object).subscribe(_ => {
|
||||||
activeModal.close()
|
activeModal.close()
|
||||||
this.reloadData()
|
this.reloadData()
|
||||||
|
}, error => {
|
||||||
|
activeModal.componentInstance.buttonsEnabled = true
|
||||||
|
this.toastService.showError($localize`Error while deleting element: ${JSON.stringify(error.error)}`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@@ -1,11 +1,12 @@
|
|||||||
<app-page-header title="Logs">
|
<app-page-header title="Logs" i18n-title>
|
||||||
|
|
||||||
<div ngbDropdown class="btn-group">
|
<div ngbDropdown class="btn-group">
|
||||||
<button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle>
|
<button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle>
|
||||||
<svg class="toolbaricon" fill="currentColor">
|
<svg class="toolbaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#funnel" />
|
<use xlink:href="assets/bootstrap-icons.svg#funnel" />
|
||||||
</svg>
|
</svg> <ng-container i18n>Filter</ng-container>
|
||||||
Filter
|
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
|
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
|
||||||
<button *ngFor="let f of getLevels()" ngbDropdownItem (click)="setLevel(f.id)"
|
<button *ngFor="let f of getLevels()" ngbDropdownItem (click)="setLevel(f.id)"
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user