From 572e40ca2713bc5d79fc3ee5fb852b0c7b14d6db Mon Sep 17 00:00:00 2001 From: Jonas Winkler Date: Sat, 7 Nov 2020 11:30:45 +0100 Subject: [PATCH 0001/1708] backend that supports asgi and status update sockets with channels --- Pipfile | 2 + Pipfile.lock | 582 +++++++++++++++++++++++++---- src/documents/consumer.py | 44 ++- src/documents/parsers.py | 3 +- src/paperless/asgi.py | 37 ++ src/paperless/settings.py | 12 + src/paperless_tesseract/parsers.py | 25 +- 7 files changed, 613 insertions(+), 92 deletions(-) create mode 100644 src/paperless/asgi.py diff --git a/Pipfile b/Pipfile index e8f862578..d526ae252 100644 --- a/Pipfile +++ b/Pipfile @@ -26,6 +26,8 @@ fuzzywuzzy = "*" python-Levenshtein = "*" django-extensions = "" watchdog = "*" +channels = "~=3.0" +channels-redis = "*" [dev-packages] coveralls = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 8b3bf705a..642e38214 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "2c1558fe7df0aee1ee20b095c2102f802470bf4a4ae09a7749ac487f8bfab8b6" + "sha256": "192d7419b844e6bb81fed793e7766b2ba15f2a016af1a33fc73cf09e12de5fb7" }, "pipfile-spec": 6, "requires": {}, @@ -14,13 +14,151 @@ ] }, "default": { + "aioredis": { + "hashes": [ + "sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a", + "sha256:b61808d7e97b7cd5a92ed574937a079c9387fdadd22bfbfa7ad2fd319ecc26e3" + ], + "version": "==1.3.1" + }, "asgiref": { "hashes": [ - "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", - "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed" + "sha256:a5098bc870b80e7b872bff60bb363c7f2c2c89078759f6c47b53ff8c525a152e", + "sha256:cd88907ecaec59d78e4ac00ea665b03e571cb37e3a0e37b3702af1a9e86c365a" ], "markers": "python_version >= '3.5'", - "version": "==3.2.10" + "version": "==3.3.0" + }, + "async-timeout": { + "hashes": [ + "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", + "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" + ], + "markers": "python_full_version >= '3.5.3'", + "version": "==3.0.1" + }, + "attrs": { + "hashes": [ + "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", + "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.3.0" + }, + "autobahn": { + "hashes": [ + "sha256:24ce276d313e84d68241c3aef30d484f352b90a40168981b3640312c821df77b", + "sha256:86bbce30cdd407137c57670993a8f9bfdfe3f8e994b889181d85e844d5aa8dfb" + ], + "markers": "python_version >= '3.5'", + "version": "==20.7.1" + }, + "automat": { + "hashes": [ + "sha256:7979803c74610e11ef0c0d68a2942b152df52da55336e0c9d58daf1831cbdf33", + "sha256:b6feb6455337df834f6c9962d6ccf771515b7d939bca142b29c20c2376bc6111" + ], + "version": "==20.2.0" + }, + "cffi": { + "hashes": [ + "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d", + "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b", + "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4", + "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f", + "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3", + "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579", + "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537", + "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e", + "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05", + "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171", + "sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca", + "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522", + "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c", + "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc", + "sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d", + "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808", + "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828", + "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869", + "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d", + "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9", + "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0", + "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc", + "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15", + "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c", + "sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a", + "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3", + "sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1", + "sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768", + "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d", + "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b", + "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e", + "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d", + "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730", + "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394", + "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1", + "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591" + ], + "version": "==1.14.3" + }, + "channels": { + "hashes": [ + "sha256:5cdd9c6b9ee663cdf1bbb00de7cdab885a3c418f9d32a29f04b09498828020f6", + "sha256:b02e150b48704ec3607d4168402ac5c26138dd183fcdb7f2aeb965e6e19fd558" + ], + "index": "pypi", + "version": "==3.0.1" + }, + "channels-redis": { + "hashes": [ + "sha256:18d63f6462a58011740dc8eeb57ea4b31ec220eb551cb71b27de9c6779a549de", + "sha256:2fb31a63b05373f6402da2e6a91a22b9e66eb8b56626c6bfc93e156c734c5ae6" + ], + "index": "pypi", + "version": "==3.2.0" + }, + "constantly": { + "hashes": [ + "sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35", + "sha256:dd2fa9d6b1a51a83f0d7dd76293d734046aa176e384bf6e33b7e44880eb37c5d" + ], + "version": "==15.1.0" + }, + "cryptography": { + "hashes": [ + "sha256:07ca431b788249af92764e3be9a488aa1d39a0bc3be313d826bbec690417e538", + "sha256:13b88a0bd044b4eae1ef40e265d006e34dbcde0c2f1e15eb9896501b2d8f6c6f", + "sha256:32434673d8505b42c0de4de86da8c1620651abd24afe91ae0335597683ed1b77", + "sha256:3cd75a683b15576cfc822c7c5742b3276e50b21a06672dc3a800a2d5da4ecd1b", + "sha256:4e7268a0ca14536fecfdf2b00297d4e407da904718658c1ff1961c713f90fd33", + "sha256:545a8550782dda68f8cdc75a6e3bf252017aa8f75f19f5a9ca940772fc0cb56e", + "sha256:55d0b896631412b6f0c7de56e12eb3e261ac347fbaa5d5e705291a9016e5f8cb", + "sha256:5849d59358547bf789ee7e0d7a9036b2d29e9a4ddf1ce5e06bb45634f995c53e", + "sha256:6dc59630ecce8c1f558277ceb212c751d6730bd12c80ea96b4ac65637c4f55e7", + "sha256:7117319b44ed1842c617d0a452383a5a052ec6aa726dfbaffa8b94c910444297", + "sha256:75e8e6684cf0034f6bf2a97095cb95f81537b12b36a8fedf06e73050bb171c2d", + "sha256:7b8d9d8d3a9bd240f453342981f765346c87ade811519f98664519696f8e6ab7", + "sha256:a035a10686532b0587d58a606004aa20ad895c60c4d029afa245802347fab57b", + "sha256:a4e27ed0b2504195f855b52052eadcc9795c59909c9d84314c5408687f933fc7", + "sha256:a733671100cd26d816eed39507e585c156e4498293a907029969234e5e634bc4", + "sha256:a75f306a16d9f9afebfbedc41c8c2351d8e61e818ba6b4c40815e2b5740bb6b8", + "sha256:bd717aa029217b8ef94a7d21632a3bb5a4e7218a4513d2521c2a2fd63011e98b", + "sha256:d25cecbac20713a7c3bc544372d42d8eafa89799f492a43b79e1dfd650484851", + "sha256:d26a2557d8f9122f9bf445fc7034242f4375bd4e95ecda007667540270965b13", + "sha256:d3545829ab42a66b84a9aaabf216a4dce7f16dbc76eb69be5c302ed6b8f4a29b", + "sha256:d3d5e10be0cf2a12214ddee45c6bd203dab435e3d83b4560c03066eda600bfe3", + "sha256:efe15aca4f64f3a7ea0c09c87826490e50ed166ce67368a68f315ea0807a20df" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==3.2.1" + }, + "daphne": { + "hashes": [ + "sha256:60856f7efa0b1e1b969efa074e8698bd09de4713ecc06e6a4d19d04c66c4a3bd", + "sha256:b43e70d74ff832a634ff6c92badd208824e4530e08b340116517e5aad0aca774" + ], + "markers": "python_version >= '3.6'", + "version": "==3.0.0" }, "dateparser": { "hashes": [ @@ -32,11 +170,11 @@ }, "django": { "hashes": [ - "sha256:a2127ad0150ec6966655bedf15dbbff9697cc86d61653db2da1afa506c0b04cc", - "sha256:c93c28ccf1d094cbd00d860e83128a39e45d2c571d3b54361713aaaf9a94cac4" + "sha256:14a4b7cd77297fba516fc0d92444cc2e2e388aa9de32d7a68d4a83d58f5a4927", + "sha256:14b87775ffedab2ef6299b73343d1b4b41e5d4e2aa58c6581f114dbec01e3f8f" ], "index": "pypi", - "version": "==3.1.2" + "version": "==3.1.3" }, "django-cors-headers": { "hashes": [ @@ -65,11 +203,10 @@ }, "djangorestframework": { "hashes": [ - "sha256:5c5071fcbad6dce16f566d492015c829ddb0df42965d488b878594aabc3aed21", - "sha256:d54452aedebb4b650254ca092f9f4f5df947cb1de6ab245d817b08b4f4156249" + "sha256:0209bafcb7b5010fdfec784034f059d512256424de2a0f084cb82b096d6dd6a7" ], "index": "pypi", - "version": "==3.12.1" + "version": "==3.12.2" }, "filemagic": { "hashes": [ @@ -94,6 +231,80 @@ "index": "pypi", "version": "==20.0.4" }, + "hiredis": { + "hashes": [ + "sha256:06a039208f83744a702279b894c8cf24c14fd63c59cd917dcde168b79eef0680", + "sha256:0a909bf501459062aa1552be1461456518f367379fdc9fdb1f2ca5e4a1fdd7c0", + "sha256:18402d9e54fb278cb9a8c638df6f1550aca36a009d47ecf5aa263a38600f35b0", + "sha256:1e4cbbc3858ec7e680006e5ca590d89a5e083235988f26a004acf7244389ac01", + "sha256:23344e3c2177baf6975fbfa361ed92eb7d36d08f454636e5054b3faa7c2aff8a", + "sha256:289b31885b4996ce04cadfd5fc03d034dce8e2a8234479f7c9e23b9e245db06b", + "sha256:2c1c570ae7bf1bab304f29427e2475fe1856814312c4a1cf1cd0ee133f07a3c6", + "sha256:2c227c0ed371771ffda256034427320870e8ea2e4fd0c0a618c766e7c49aad73", + "sha256:3bb9b63d319402cead8bbd9dd55dca3b667d2997e9a0d8a1f9b6cc274db4baee", + "sha256:3ef2183de67b59930d2db8b8e8d4d58e00a50fcc5e92f4f678f6eed7a1c72d55", + "sha256:43b8ed3dbfd9171e44c554cb4acf4ee4505caa84c5e341858b50ea27dd2b6e12", + "sha256:47bcf3c5e6c1e87ceb86cdda2ee983fa0fe56a999e6185099b3c93a223f2fa9b", + "sha256:5263db1e2e1e8ae30500cdd75a979ff99dcc184201e6b4b820d0de74834d2323", + "sha256:5b1451727f02e7acbdf6aae4e06d75f66ee82966ff9114550381c3271a90f56c", + "sha256:6996883a8a6ff9117cbb3d6f5b0dcbbae6fb9e31e1a3e4e2f95e0214d9a1c655", + "sha256:6c96f64a54f030366657a54bb90b3093afc9c16c8e0dfa29fc0d6dbe169103a5", + "sha256:7332d5c3e35154cd234fd79573736ddcf7a0ade7a986db35b6196b9171493e75", + "sha256:7885b6f32c4a898e825bb7f56f36a02781ac4a951c63e4169f0afcf9c8c30dfb", + "sha256:7b0f63f10a166583ab744a58baad04e0f52cfea1ac27bfa1b0c21a48d1003c23", + "sha256:819f95d4eba3f9e484dd115ab7ab72845cf766b84286a00d4ecf76d33f1edca1", + "sha256:8968eeaa4d37a38f8ca1f9dbe53526b69628edc9c42229a5b2f56d98bb828c1f", + "sha256:89ebf69cb19a33d625db72d2ac589d26e936b8f7628531269accf4a3196e7872", + "sha256:8daecd778c1da45b8bd54fd41ffcd471a86beed3d8e57a43acf7a8d63bba4058", + "sha256:955ba8ea73cf3ed8bd2f963b4cb9f8f0dcb27becd2f4b3dd536fd24c45533454", + "sha256:964f18a59f5a64c0170f684c417f4fe3e695a536612e13074c4dd5d1c6d7c882", + "sha256:969843fbdfbf56cdb71da6f0bdf50f9985b8b8aeb630102945306cf10a9c6af2", + "sha256:996021ef33e0f50b97ff2d6b5f422a0fe5577de21a8873b58a779a5ddd1c3132", + "sha256:9e9c9078a7ce07e6fce366bd818be89365a35d2e4b163268f0ca9ba7e13bb2f6", + "sha256:a04901757cb0fb0f5602ac11dda48f5510f94372144d06c2563ba56c480b467c", + "sha256:a7bf1492429f18d205f3a818da3ff1f242f60aa59006e53dee00b4ef592a3363", + "sha256:aa0af2deb166a5e26e0d554b824605e660039b161e37ed4f01b8d04beec184f3", + "sha256:abfb15a6a7822f0fae681785cb38860e7a2cb1616a708d53df557b3d76c5bfd4", + "sha256:b253fe4df2afea4dfa6b1fa8c5fef212aff8bcaaeb4207e81eed05cb5e4a7919", + "sha256:b27f082f47d23cffc4cf1388b84fdc45c4ef6015f906cd7e0d988d9e35d36349", + "sha256:b33aea449e7f46738811fbc6f0b3177c6777a572207412bbbf6f525ffed001ae", + "sha256:b44f9421c4505c548435244d74037618f452844c5d3c67719d8a55e2613549da", + "sha256:bcc371151d1512201d0214c36c0c150b1dc64f19c2b1a8c9cb1d7c7c15ebd93f", + "sha256:c2851deeabd96d3f6283e9c6b26e0bfed4de2dc6fb15edf913e78b79fc5909ed", + "sha256:cdfd501c7ac5b198c15df800a3a34c38345f5182e5f80770caf362bccca65628", + "sha256:d2c0caffa47606d6d7c8af94ba42547bd2a441f06c74fd90a1ffe328524a6c64", + "sha256:dcb2db95e629962db5a355047fb8aefb012df6c8ae608930d391619dbd96fd86", + "sha256:e0eeb9c112fec2031927a1745788a181d0eecbacbed941fc5c4f7bc3f7b273bf", + "sha256:e154891263306200260d7f3051982774d7b9ef35af3509d5adbbe539afd2610c", + "sha256:e2e023a42dcbab8ed31f97c2bcdb980b7fbe0ada34037d87ba9d799664b58ded", + "sha256:e64be68255234bb489a574c4f2f8df7029c98c81ec4d160d6cd836e7f0679390", + "sha256:e82d6b930e02e80e5109b678c663a9ed210680ded81c1abaf54635d88d1da298" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.1.0" + }, + "hyperlink": { + "hashes": [ + "sha256:47fcc7cd339c6cb2444463ec3277bdcfe142c8b1daf2160bdd52248deec815af", + "sha256:c528d405766f15a2c536230de7e160b65a08e20264d8891b3eb03307b0df3c63" + ], + "version": "==20.0.1" + }, + "idna": { + "hashes": [ + "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" + }, + "incremental": { + "hashes": [ + "sha256:717e12246dddf231a349175f48d74d93e2897244939173b01974ab6661406b9f", + "sha256:7b751696aaf36eebfab537e458929e194460051ccad279c72b755a167eebd4b3" + ], + "version": "==17.5.0" + }, "joblib": { "hashes": [ "sha256:698c311779f347cf6b7e6b8a39bb682277b8ee4aba8cf9507bc0cf4cd4737b72", @@ -110,45 +321,68 @@ "index": "pypi", "version": "==1.0.8" }, + "msgpack": { + "hashes": [ + "sha256:002a0d813e1f7b60da599bdf969e632074f9eec1b96cbed8fb0973a63160a408", + "sha256:25b3bc3190f3d9d965b818123b7752c5dfb953f0d774b454fd206c18fe384fb8", + "sha256:271b489499a43af001a2e42f42d876bb98ccaa7e20512ff37ca78c8e12e68f84", + "sha256:39c54fdebf5fa4dda733369012c59e7d085ebdfe35b6cf648f09d16708f1be5d", + "sha256:4233b7f86c1208190c78a525cd3828ca1623359ef48f78a6fea4b91bb995775a", + "sha256:5bea44181fc8e18eed1d0cd76e355073f00ce232ff9653a0ae88cb7d9e643322", + "sha256:5dba6d074fac9b24f29aaf1d2d032306c27f04187651511257e7831733293ec2", + "sha256:7a22c965588baeb07242cb561b63f309db27a07382825fc98aecaf0827c1538e", + "sha256:908944e3f038bca67fcfedb7845c4a257c7749bf9818632586b53bcf06ba4b97", + "sha256:9534d5cc480d4aff720233411a1f765be90885750b07df772380b34c10ecb5c0", + "sha256:aa5c057eab4f40ec47ea6f5a9825846be2ff6bf34102c560bad5cad5a677c5be", + "sha256:b3758dfd3423e358bbb18a7cccd1c74228dffa7a697e5be6cb9535de625c0dbf", + "sha256:c901e8058dd6653307906c5f157f26ed09eb94a850dddd989621098d347926ab", + "sha256:cec8bf10981ed70998d98431cd814db0ecf3384e6b113366e7f36af71a0fca08", + "sha256:db685187a415f51d6b937257474ca72199f393dad89534ebbdd7d7a3b000080e", + "sha256:e35b051077fc2f3ce12e7c6a34cf309680c63a842db3a0616ea6ed25ad20d272", + "sha256:e7bbdd8e2b277b77782f3ce34734b0dfde6cbe94ddb74de8d733d603c7f9e2b1", + "sha256:ea41c9219c597f1d2bf6b374d951d310d58684b5de9dc4bd2976db9e1e22c140" + ], + "version": "==1.0.0" + }, "numpy": { "hashes": [ - "sha256:0ee77786eebbfa37f2141fd106b549d37c89207a0d01d8852fde1c82e9bfc0e7", - "sha256:199bebc296bd8a5fc31c16f256ac873dd4d5b4928dfd50e6c4995570fc71a8f3", - "sha256:1a307bdd3dd444b1d0daa356b5f4c7de2e24d63bdc33ea13ff718b8ec4c6a268", - "sha256:1ea7e859f16e72ab81ef20aae69216cfea870676347510da9244805ff9670170", - "sha256:271139653e8b7a046d11a78c0d33bafbddd5c443a5b9119618d0652a4eb3a09f", - "sha256:35bf5316af8dc7c7db1ad45bec603e5fb28671beb98ebd1d65e8059efcfd3b72", - "sha256:463792a249a81b9eb2b63676347f996d3f0082c2666fd0604f4180d2e5445996", - "sha256:50d3513469acf5b2c0406e822d3f314d7ac5788c2b438c24e5dd54d5a81ef522", - "sha256:50f68ebc439821b826823a8da6caa79cd080dee2a6d5ab9f1163465a060495ed", - "sha256:51e8d2ae7c7e985c7bebf218e56f72fa93c900ad0c8a7d9fbbbf362f45710f69", - "sha256:522053b731e11329dd52d258ddf7de5288cae7418b55e4b7d32f0b7e31787e9d", - "sha256:5ea4401ada0d3988c263df85feb33818dc995abc85b8125f6ccb762009e7bc68", - "sha256:604d2e5a31482a3ad2c88206efd43d6fcf666ada1f3188fd779b4917e49b7a98", - "sha256:6ff88bcf1872b79002569c63fe26cd2cda614e573c553c4d5b814fb5eb3d2822", - "sha256:7197ee0a25629ed782c7bd01871ee40702ffeef35bc48004bc2fdcc71e29ba9d", - "sha256:741d95eb2b505bb7a99fbf4be05fa69f466e240c2b4f2d3ddead4f1b5f82a5a5", - "sha256:83af653bb92d1e248ccf5fdb05ccc934c14b936bcfe9b917dc180d3f00250ac6", - "sha256:8802d23e4895e0c65e418abe67cdf518aa5cbb976d97f42fd591f921d6dffad0", - "sha256:8edc4d687a74d0a5f8b9b26532e860f4f85f56c400b3a98899fc44acb5e27add", - "sha256:942d2cdcb362739908c26ce8dd88db6e139d3fa829dd7452dd9ff02cba6b58b2", - "sha256:9a0669787ba8c9d3bb5de5d9429208882fb47764aa79123af25c5edc4f5966b9", - "sha256:9d08d84bb4128abb9fbd9f073e5c69f70e5dab991a9c42e5b4081ea5b01b5db0", - "sha256:9f7f56b5e85b08774939622b7d45a5d00ff511466522c44fc0756ac7692c00f2", - "sha256:a2daea1cba83210c620e359de2861316f49cc7aea8e9a6979d6cb2ddab6dda8c", - "sha256:b9074d062d30c2779d8af587924f178a539edde5285d961d2dfbecbac9c4c931", - "sha256:c4aa79993f5d856765819a3651117520e41ac3f89c3fc1cb6dee11aa562df6da", - "sha256:d78294f1c20f366cde8a75167f822538a7252b6e8b9d6dbfb3bdab34e7c1929e", - "sha256:dfdc8b53aa9838b9d44ed785431ca47aa3efaa51d0d5dd9c412ab5247151a7c4", - "sha256:dffed17848e8b968d8d3692604e61881aa6ef1f8074c99e81647ac84f6038535", - "sha256:e080087148fd70469aade2abfeadee194357defd759f9b59b349c6192aba994c", - "sha256:e983cbabe10a8989333684c98fdc5dd2f28b236216981e0c26ed359aaa676772", - "sha256:ea6171d2d8d648dee717457d0f75db49ad8c2f13100680e284d7becf3dc311a6", - "sha256:eefc13863bf01583a85e8c1121a901cc7cb8f059b960c4eba30901e2e6aba95f", - "sha256:efd656893171bbf1331beca4ec9f2e74358fc732a2084f664fd149cc4b3441d2" + "sha256:08308c38e44cc926bdfce99498b21eec1f848d24c302519e64203a8da99a97db", + "sha256:09c12096d843b90eafd01ea1b3307e78ddd47a55855ad402b157b6c4862197ce", + "sha256:13d166f77d6dc02c0a73c1101dd87fdf01339febec1030bd810dcd53fff3b0f1", + "sha256:141ec3a3300ab89c7f2b0775289954d193cc8edb621ea05f99db9cb181530512", + "sha256:16c1b388cc31a9baa06d91a19366fb99ddbe1c7b205293ed072211ee5bac1ed2", + "sha256:18bed2bcb39e3f758296584337966e68d2d5ba6aab7e038688ad53c8f889f757", + "sha256:1aeef46a13e51931c0b1cf8ae1168b4a55ecd282e6688fdb0a948cc5a1d5afb9", + "sha256:27d3f3b9e3406579a8af3a9f262f5339005dd25e0ecf3cf1559ff8a49ed5cbf2", + "sha256:2a2740aa9733d2e5b2dfb33639d98a64c3b0f24765fed86b0fd2aec07f6a0a08", + "sha256:4377e10b874e653fe96985c05feed2225c912e328c8a26541f7fc600fb9c637b", + "sha256:448ebb1b3bf64c0267d6b09a7cba26b5ae61b6d2dbabff7c91b660c7eccf2bdb", + "sha256:50e86c076611212ca62e5a59f518edafe0c0730f7d9195fec718da1a5c2bb1fc", + "sha256:5734bdc0342aba9dfc6f04920988140fb41234db42381cf7ccba64169f9fe7ac", + "sha256:64324f64f90a9e4ef732be0928be853eee378fd6a01be21a0a8469c4f2682c83", + "sha256:6ae6c680f3ebf1cf7ad1d7748868b39d9f900836df774c453c11c5440bc15b36", + "sha256:6d7593a705d662be5bfe24111af14763016765f43cb6923ed86223f965f52387", + "sha256:8cac8790a6b1ddf88640a9267ee67b1aee7a57dfa2d2dd33999d080bc8ee3a0f", + "sha256:8ece138c3a16db8c1ad38f52eb32be6086cc72f403150a79336eb2045723a1ad", + "sha256:9eeb7d1d04b117ac0d38719915ae169aa6b61fca227b0b7d198d43728f0c879c", + "sha256:a09f98011236a419ee3f49cedc9ef27d7a1651df07810ae430a6b06576e0b414", + "sha256:a5d897c14513590a85774180be713f692df6fa8ecf6483e561a6d47309566f37", + "sha256:ad6f2ff5b1989a4899bf89800a671d71b1612e5ff40866d1f4d8bcf48d4e5764", + "sha256:c42c4b73121caf0ed6cd795512c9c09c52a7287b04d105d112068c1736d7c753", + "sha256:cb1017eec5257e9ac6209ac172058c430e834d5d2bc21961dceeb79d111e5909", + "sha256:d6c7bb82883680e168b55b49c70af29b84b84abb161cbac2800e8fcb6f2109b6", + "sha256:e452dc66e08a4ce642a961f134814258a082832c78c90351b75c41ad16f79f63", + "sha256:e5b6ed0f0b42317050c88022349d994fe72bfe35f5908617512cd8c8ef9da2a9", + "sha256:e9b30d4bd69498fc0c3fe9db5f62fffbb06b8eb9321f92cc970f2969be5e3949", + "sha256:ec149b90019852266fec2341ce1db513b843e496d5a8e8cdb5ced1923a92faab", + "sha256:edb01671b3caae1ca00881686003d16c2209e07b7ef8b7639f1867852b948f7c", + "sha256:f0d3929fe88ee1c155129ecd82f981b8856c5d97bcb0d5f23e9b4242e79d1de3", + "sha256:f29454410db6ef8126c83bd3c968d143304633d45dc57b51252afbd79d700893", + "sha256:fe45becb4c2f72a0907c1d0246ea6449fe7a9e2293bb0e11c4e9a32bb0930a15", + "sha256:fedbd128668ead37f33917820b704784aff695e0019309ad446a6d0b065b57e4" ], "markers": "python_version >= '3.6'", - "version": "==1.19.3" + "version": "==1.19.4" }, "pathtools": { "hashes": [ @@ -202,9 +436,11 @@ "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c", "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67", "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0", + "sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6", "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db", "sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94", "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52", + "sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056", "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b", "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd", "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550", @@ -236,6 +472,58 @@ "index": "pypi", "version": "==2.8.6" }, + "pyasn1": { + "hashes": [ + "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", + "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576", + "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", + "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", + "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", + "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", + "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", + "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86", + "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12", + "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776", + "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", + "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2", + "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3" + ], + "version": "==0.4.8" + }, + "pyasn1-modules": { + "hashes": [ + "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8", + "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199", + "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811", + "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed", + "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4", + "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", + "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74", + "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb", + "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45", + "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd", + "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0", + "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d", + "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405" + ], + "version": "==0.2.8" + }, + "pycparser": { + "hashes": [ + "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", + "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.20" + }, + "pyhamcrest": { + "hashes": [ + "sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316", + "sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29" + ], + "markers": "python_version >= '3.5'", + "version": "==2.0.2" + }, "pyocr": { "hashes": [ "sha256:fa15adc7e1cf0d345a2990495fe125a947c6e09a60ddba0256a1c14b2e603179" @@ -243,6 +531,13 @@ "index": "pypi", "version": "==0.7.2" }, + "pyopenssl": { + "hashes": [ + "sha256:621880965a720b8ece2f1b2f54ea2071966ab00e2970ad2ce11d596102063504", + "sha256:9a24494b2602aaf402be5c9e30a0b82d4a5c67528fe8fb475e3f3bc00dd69507" + ], + "version": "==19.1.0" + }, "python-dateutil": { "hashes": [ "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", @@ -276,10 +571,10 @@ }, "pytz": { "hashes": [ - "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", - "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" + "sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268", + "sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd" ], - "version": "==2020.1" + "version": "==2020.4" }, "regex": { "hashes": [ @@ -287,26 +582,42 @@ "sha256:06b52815d4ad38d6524666e0d50fe9173533c9cc145a5779b89733284e6f688f", "sha256:11116d424734fe356d8777f89d625f0df783251ada95d6261b4c36ad27a394bb", "sha256:119e0355dbdd4cf593b17f2fc5dbd4aec2b8899d0057e4957ba92f941f704bf5", + "sha256:127a9e0c0d91af572fbb9e56d00a504dbd4c65e574ddda3d45b55722462210de", "sha256:1ec66700a10e3c75f1f92cbde36cca0d3aaee4c73dfa26699495a3a30b09093c", + "sha256:227a8d2e5282c2b8346e7f68aa759e0331a0b4a890b55a5cfbb28bd0261b84c0", + "sha256:2564def9ce0710d510b1fc7e5178ce2d20f75571f788b5197b3c8134c366f50c", + "sha256:297116e79074ec2a2f885d22db00ce6e88b15f75162c5e8b38f66ea734e73c64", "sha256:2dc522e25e57e88b4980d2bdd334825dbf6fa55f28a922fc3bfa60cc09e5ef53", "sha256:3a5f08039eee9ea195a89e180c5762bfb55258bfb9abb61a20d3abee3b37fd12", + "sha256:3dfca201fa6b326239e1bccb00b915e058707028809b8ecc0cf6819ad233a740", "sha256:49461446b783945597c4076aea3f49aee4b4ce922bd241e4fcf62a3e7c61794c", "sha256:4afa350f162551cf402bfa3cd8302165c8e03e689c897d185f16a167328cc6dd", "sha256:4b5a9bcb56cc146c3932c648603b24514447eafa6ce9295234767bf92f69b504", + "sha256:52e83a5f28acd621ba8e71c2b816f6541af7144b69cc5859d17da76c436a5427", "sha256:625116aca6c4b57c56ea3d70369cacc4d62fead4930f8329d242e4fe7a58ce4b", "sha256:654c1635f2313d0843028487db2191530bca45af61ca85d0b16555c399625b0e", "sha256:8092a5a06ad9a7a247f2a76ace121183dc4e1a84c259cf9c2ce3bbb69fac3582", "sha256:832339223b9ce56b7b15168e691ae654d345ac1635eeb367ade9ecfe0e66bee0", "sha256:8ca9dca965bd86ea3631b975d63b0693566d3cc347e55786d5514988b6f5b84c", + "sha256:96f99219dddb33e235a37283306834700b63170d7bb2a1ee17e41c6d589c8eb9", + "sha256:9b6305295b6591e45f069d3553c54d50cc47629eb5c218aac99e0f7fafbf90a1", "sha256:a62162be05edf64f819925ea88d09d18b09bebf20971b363ce0c24e8b4aa14c0", + "sha256:aacc8623ffe7999a97935eeabbd24b1ae701d08ea8f874a6ff050e93c3e658cf", + "sha256:b45bab9f224de276b7bc916f6306b86283f6aa8afe7ed4133423efb42015a898", "sha256:b88fa3b8a3469f22b4f13d045d9bd3eda797aa4e406fde0a2644bc92bbdd4bdd", + "sha256:b8a686a6c98872007aa41fdbb2e86dc03b287d951ff4a7f1da77fb7f14113e4d", + "sha256:bd904c0dec29bbd0769887a816657491721d5f545c29e30fd9d7a1a275dc80ab", + "sha256:bf4f896c42c63d1f22039ad57de2644c72587756c0cfb3cc3b7530cfe228277f", "sha256:c13d311a4c4a8d671f5860317eb5f09591fbe8259676b86a85769423b544451e", "sha256:c2c6c56ee97485a127555c9595c069201b5161de9d05495fbe2132b5ac104786", + "sha256:c32c91a0f1ac779cbd73e62430de3d3502bbc45ffe5bb6c376015acfa848144b", "sha256:c3466a84fce42c2016113101018a9981804097bacbab029c2d5b4fcb224b89de", + "sha256:c454ad88e56e80e44f824ef8366bb7e4c3def12999151fd5c0ea76a18fe9aa3e", "sha256:c8a2b7ccff330ae4c460aff36626f911f918555660cc28163417cb84ffb25789", "sha256:cb905f3d2e290a8b8f1579d3984f2cfa7c3a29cc7cba608540ceeed18513f520", "sha256:cfcf28ed4ce9ced47b9b9670a4f0d3d3c0e4d4779ad4dadb1ad468b097f808aa", "sha256:dd3e6547ecf842a29cf25123fbf8d2461c53c8d37aa20d87ecee130c89b7079b", + "sha256:de7fd57765398d141949946c84f3590a68cf5887dac3fc52388df0639b01eda4", "sha256:ea37320877d56a7f0a1e6a625d892cf963aa7f570013499f5b8d5ab8402b5625", "sha256:f1fce1e4929157b2afeb4bb7069204d4370bab9f4fc03ca1fbec8bd601f8c87d", "sha256:f43109822df2d3faac7aad79613f5f02e4eab0fc8ad7932d2e70e2a83bd49c26" @@ -337,28 +648,41 @@ }, "scipy": { "hashes": [ - "sha256:07b083128beae040f1129bd8a82b01804f5e716a7fd2962c1053fa683433e4ab", - "sha256:0edd67e8a00903aaf7a29c968555a2e27c5a69fea9d1dcfffda80614281a884f", - "sha256:12fdcbfa56cac926a0a9364a30cbf4ad03c2c7b59f75b14234656a5e4fd52bf3", - "sha256:1fee28b6641ecbff6e80fe7788e50f50c5576157d278fa40f36c851940eb0aff", - "sha256:33e6a7439f43f37d4c1135bc95bcd490ffeac6ef4b374892c7005ce2c729cf4a", - "sha256:5163200ab14fd2b83aba8f0c4ddcc1fa982a43192867264ab0f4c8065fd10d17", - "sha256:66ec29348444ed6e8a14c9adc2de65e74a8fc526dc2c770741725464488ede1f", - "sha256:8cc5c39ed287a8b52a5509cd6680af078a40b0e010e2657eca01ffbfec929468", - "sha256:a1a13858b10d41beb0413c4378462b43eafef88a1948d286cb357eadc0aec024", - "sha256:a3db1fe7c6cb29ca02b14c9141151ebafd11e06ffb6da8ecd330eee5c8283a8a", - "sha256:aebb69bcdec209d874fc4b0c7ac36f509d50418a431c1422465fa34c2c0143ea", - "sha256:b9751b39c52a3fa59312bd2e1f40144ee26b51404db5d2f0d5259c511ff6f614", - "sha256:bc0e63daf43bf052aefbbd6c5424bc03f629d115ece828e87303a0bcc04a37e4", - "sha256:d5e3cc60868f396b78fc881d2c76460febccfe90f6d2f082b9952265c79a8788", - "sha256:ddae76784574cc4c172f3d5edd7308be16078dd3b977e8746860c76c195fa707", - "sha256:e2602f79c85924e4486f684aa9bbab74afff90606100db88d0785a0088be7edb", - "sha256:e527c9221b6494bcd06a17f9f16874406b32121385f9ab353b8a9545be458f0b", - "sha256:f574558f1b774864516f3c3fe072ebc90a29186f49b720f60ed339294b7f32ac", - "sha256:ffcbd331f1ffa82e22f1d408e93c37463c9a83088243158635baec61983aaacf" + "sha256:168c45c0c32e23f613db7c9e4e780bc61982d71dcd406ead746c7c7c2f2004ce", + "sha256:213bc59191da2f479984ad4ec39406bf949a99aba70e9237b916ce7547b6ef42", + "sha256:25b241034215247481f53355e05f9e25462682b13bd9191359075682adcd9554", + "sha256:2c872de0c69ed20fb1a9b9cf6f77298b04a26f0b8720a5457be08be254366c6e", + "sha256:3397c129b479846d7eaa18f999369a24322d008fac0782e7828fa567358c36ce", + "sha256:368c0f69f93186309e1b4beb8e26d51dd6f5010b79264c0f1e9ca00cd92ea8c9", + "sha256:3d5db5d815370c28d938cf9b0809dade4acf7aba57eaf7ef733bfedc9b2474c4", + "sha256:4598cf03136067000855d6b44d7a1f4f46994164bcd450fb2c3d481afc25dd06", + "sha256:4a453d5e5689de62e5d38edf40af3f17560bfd63c9c5bd228c18c1f99afa155b", + "sha256:4f12d13ffbc16e988fa40809cbbd7a8b45bc05ff6ea0ba8e3e41f6f4db3a9e47", + "sha256:634568a3018bc16a83cda28d4f7aed0d803dd5618facb36e977e53b2df868443", + "sha256:65923bc3809524e46fb7eb4d6346552cbb6a1ffc41be748535aa502a2e3d3389", + "sha256:6b0ceb23560f46dd236a8ad4378fc40bad1783e997604ba845e131d6c680963e", + "sha256:8c8d6ca19c8497344b810b0b0344f8375af5f6bb9c98bd42e33f747417ab3f57", + "sha256:9ad4fcddcbf5dc67619379782e6aeef41218a79e17979aaed01ed099876c0e62", + "sha256:a254b98dbcc744c723a838c03b74a8a34c0558c9ac5c86d5561703362231107d", + "sha256:b03c4338d6d3d299e8ca494194c0ae4f611548da59e3c038813f1a43976cb437", + "sha256:cc1f78ebc982cd0602c9a7615d878396bec94908db67d4ecddca864d049112f2", + "sha256:d6d25c41a009e3c6b7e757338948d0076ee1dd1770d1c09ec131f11946883c54", + "sha256:d84cadd7d7998433334c99fa55bcba0d8b4aeff0edb123b2a1dfcface538e474", + "sha256:e360cb2299028d0b0d0f65a5c5e51fc16a335f1603aa2357c25766c8dab56938", + "sha256:e98d49a5717369d8241d6cf33ecb0ca72deee392414118198a8e5b4c35c56340", + "sha256:ed572470af2438b526ea574ff8f05e7f39b44ac37f712105e57fc4d53a6fb660", + "sha256:f87b39f4d69cf7d7529d7b1098cb712033b17ea7714aed831b95628f483fd012", + "sha256:fa789583fc94a7689b45834453fec095245c7e69c58561dc159b5d5277057e4c" ], "markers": "python_version >= '3.6'", - "version": "==1.5.3" + "version": "==1.5.4" + }, + "service-identity": { + "hashes": [ + "sha256:001c0707759cb3de7e49c078a7c0c9cd12594161d3bf06b9c254fdcb1a60dc36", + "sha256:0858a54aabc5b459d1aafa8a518ed2081a285087f349fe3e55197989232e2e2d" + ], + "version": "==18.1.0" }, "six": { "hashes": [ @@ -384,6 +708,46 @@ "markers": "python_version >= '3.5'", "version": "==2.1.0" }, + "twisted": { + "extras": [ + "tls" + ], + "hashes": [ + "sha256:040eb6641125d2a9a09cf198ec7b83dd8858c6f51f6770325ed9959c00f5098f", + "sha256:147780b8caf21ba2aef3688628eaf13d7e7fe02a86747cd54bfaf2140538f042", + "sha256:158ddb80719a4813d292293ac44ba41d8b56555ed009d90994a278237ee63d2c", + "sha256:2182000d6ffc05d269e6c03bfcec8b57e20259ca1086180edaedec3f1e689292", + "sha256:25ffcf37944bdad4a99981bc74006d735a678d2b5c193781254fbbb6d69e3b22", + "sha256:3281d9ce889f7b21bdb73658e887141aa45a102baf3b2320eafcfba954fcefec", + "sha256:356e8d8dd3590e790e3dba4db139eb8a17aca64b46629c622e1b1597a4a92478", + "sha256:70952c56e4965b9f53b180daecf20a9595cf22b8d0935cd3bd664c90273c3ab2", + "sha256:7408c6635ee1b96587289283ebe90ee15dbf9614b05857b446055116bc822d29", + "sha256:7c547fd0215db9da8a1bc23182b309e84a232364cc26d829e9ee196ce840b114", + "sha256:894f6f3cfa57a15ea0d0714e4283913a5f2511dbd18653dd148eba53b3919797", + "sha256:94ac3d55a58c90e2075c5fe1853f2aa3892b73e3bf56395f743aefde8605eeaa", + "sha256:a58e61a2a01e5bcbe3b575c0099a2bcb8d70a75b1a087338e0c48dd6e01a5f15", + "sha256:c09c47ff9750a8e3aa60ad169c4b95006d455a29b80ad0901f031a103b2991cd", + "sha256:ca3a0b8c9110800e576d89b5337373e52018b41069bc879f12fa42b7eb2d0274", + "sha256:cd1dc5c85b58494138a3917752b54bb1daa0045d234b7c132c37a61d5483ebad", + "sha256:cdbc4c7f0cd7a2218b575844e970f05a1be1861c607b0e048c9bceca0c4d42f7", + "sha256:d267125cc0f1e8a0eed6319ba4ac7477da9b78a535601c49ecd20c875576433a", + "sha256:d72c55b5d56e176563b91d11952d13b01af8725c623e498db5507b6614fc1e10", + "sha256:d95803193561a243cb0401b0567c6b7987d3f2a67046770e1dccd1c9e49a9780", + "sha256:e92703bed0cc21d6cb5c61d66922b3b1564015ca8a51325bd164a5e33798d504", + "sha256:f058bd0168271de4dcdc39845b52dd0a4a2fecf5f1246335f13f5e96eaebb467", + "sha256:f3c19e5bd42bbe4bf345704ad7c326c74d3fd7a1b3844987853bef180be638d4" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==20.3.0" + }, + "txaio": { + "hashes": [ + "sha256:17938f2bca4a9cabce61346758e482ca4e600160cbc28e861493eac74a19539d", + "sha256:38a469daf93c37e5527cb062653d6393ae11663147c42fab7ddc3f6d00d434ae" + ], + "markers": "python_version >= '3.5'", + "version": "==20.4.1" + }, "tzlocal": { "hashes": [ "sha256:643c97c5294aedc737780a49d9df30889321cbe1204eac2c2ec6134035a92e44", @@ -414,6 +778,64 @@ ], "index": "pypi", "version": "==2.7.4" + }, + "zope.interface": { + "hashes": [ + "sha256:05a97ba92c1c7c26f25c9f671aa1ef85ffead6cdad13770e5b689cf983adc7e1", + "sha256:07d61722dd7d85547b7c6b0f5486b4338001fab349f2ac5cabc0b7182eb3425d", + "sha256:0a990dcc97806e5980bbb54b2e46b9cde9e48932d8e6984daf71ef1745516123", + "sha256:150e8bcb7253a34a4535aeea3de36c0bb3b1a6a47a183a95d65a194b3e07f232", + "sha256:1743bcfe45af8846b775086471c28258f4c6e9ee8ef37484de4495f15a98b549", + "sha256:1b5f6c8fff4ed32aa2dd43e84061bc8346f32d3ba6ad6e58f088fe109608f102", + "sha256:21e49123f375703cf824214939d39df0af62c47d122d955b2a8d9153ea08cfd5", + "sha256:21f579134a47083ffb5ddd1307f0405c91aa8b61ad4be6fd5af0171474fe0c45", + "sha256:27c267dc38a0f0079e96a2945ee65786d38ef111e413c702fbaaacbab6361d00", + "sha256:299bde0ab9e5c4a92f01a152b7fbabb460f31343f1416f9b7b983167ab1e33bc", + "sha256:2ab88d8f228f803fcb8cb7d222c579d13dab2d3622c51e8cf321280da01102a7", + "sha256:2ced4c35061eea623bc84c7711eedce8ecc3c2c51cd9c6afa6290df3bae9e104", + "sha256:2dcab01c660983ba5e5a612e0c935141ccbee67d2e2e14b833e01c2354bd8034", + "sha256:32546af61a9a9b141ca38d971aa6eb9800450fa6620ce6323cc30eec447861f3", + "sha256:32b40a4c46d199827d79c86bb8cb88b1bbb764f127876f2cb6f3a47f63dbada3", + "sha256:3cc94c69f6bd48ed86e8e24f358cb75095c8129827df1298518ab860115269a4", + "sha256:42b278ac0989d6f5cf58d7e0828ea6b5951464e3cf2ff229dd09a96cb6ba0c86", + "sha256:495b63fd0302f282ee6c1e6ea0f1c12cb3d1a49c8292d27287f01845ff252a96", + "sha256:4af87cdc0d4b14e600e6d3d09793dce3b7171348a094ba818e2a68ae7ee67546", + "sha256:4b94df9f2fdde7b9314321bab8448e6ad5a23b80542dcab53e329527d4099dcb", + "sha256:4c48ddb63e2b20fba4c6a2bf81b4d49e99b6d4587fb67a6cd33a2c1f003af3e3", + "sha256:4df9afd17bd5477e9f8c8b6bb8507e18dd0f8b4efe73bb99729ff203279e9e3b", + "sha256:518950fe6a5d56f94ba125107895f938a4f34f704c658986eae8255edb41163b", + "sha256:538298e4e113ccb8b41658d5a4b605bebe75e46a30ceca22a5a289cf02c80bec", + "sha256:55465121e72e208a7b69b53de791402affe6165083b2ea71b892728bd19ba9ae", + "sha256:588384d70a0f19b47409cfdb10e0c27c20e4293b74fc891df3d8eb47782b8b3e", + "sha256:6278c080d4afffc9016e14325f8734456831124e8c12caa754fd544435c08386", + "sha256:64ea6c221aeee4796860405e1aedec63424cda4202a7ad27a5066876db5b0fd2", + "sha256:681dbb33e2b40262b33fd383bae63c36d33fd79fa1a8e4092945430744ffd34a", + "sha256:6936aa9da390402d646a32a6a38d5409c2d2afb2950f045a7d02ab25a4e7d08d", + "sha256:778d0ec38bbd288b150a3ae363c8ffd88d2207a756842495e9bffd8a8afbc89a", + "sha256:8251f06a77985a2729a8bdbefbae79ee78567dddc3acbd499b87e705ca59fe24", + "sha256:83b4aa5344cce005a9cff5d0321b2e318e871cc1dfc793b66c32dd4f59e9770d", + "sha256:844fad925ac5c2ad4faaceb3b2520ad016b5280105c6e16e79838cf951903a7b", + "sha256:8ceb3667dd13b8133f2e4d637b5b00f240f066448e2aa89a41f4c2d78a26ce50", + "sha256:92dc0fb79675882d0b6138be4bf0cec7ea7c7eede60aaca78303d8e8dbdaa523", + "sha256:9789bd945e9f5bd026ed3f5b453d640befb8b1fc33a779c1fe8d3eb21fe3fb4a", + "sha256:a2b6d6eb693bc2fc6c484f2e5d93bd0b0da803fa77bf974f160533e555e4d095", + "sha256:aab9f1e34d810feb00bf841993552b8fcc6ae71d473c505381627143d0018a6a", + "sha256:abb61afd84f23099ac6099d804cdba9bd3b902aaaded3ffff47e490b0a495520", + "sha256:adf9ee115ae8ff8b6da4b854b4152f253b390ba64407a22d75456fe07dcbda65", + "sha256:aedc6c672b351afe6dfe17ff83ee5e7eb6ed44718f879a9328a68bdb20b57e11", + "sha256:b7a00ecb1434f8183395fac5366a21ee73d14900082ca37cf74993cf46baa56c", + "sha256:ba32f4a91c1cb7314c429b03afbf87b1fff4fb1c8db32260e7310104bd77f0c7", + "sha256:cbd0f2cbd8689861209cd89141371d3a22a11613304d1f0736492590aa0ab332", + "sha256:e4bc372b953bf6cec65a8d48482ba574f6e051621d157cf224227dbb55486b1e", + "sha256:eccac3d9aadc68e994b6d228cb0c8919fc47a5350d85a1b4d3d81d1e98baf40c", + "sha256:efd550b3da28195746bb43bd1d815058181a7ca6d9d6aa89dd37f5eefe2cacb7", + "sha256:efef581c8ba4d990770875e1a2218e856849d32ada2680e53aebc5d154a17e20", + "sha256:f057897711a630a0b7a6a03f1acf379b6ba25d37dc5dc217a97191984ba7f2fc", + "sha256:f37d45fab14ffef9d33a0dc3bc59ce0c5313e2253323312d47739192da94f5fd", + "sha256:f44906f70205d456d503105023041f1e63aece7623b31c390a0103db4de17537" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==5.2.0" } }, "develop": { @@ -441,11 +863,11 @@ }, "attrs": { "hashes": [ - "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", - "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" + "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", + "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.2.0" + "version": "==20.3.0" }, "babel": { "hashes": [ @@ -556,11 +978,11 @@ }, "faker": { "hashes": [ - "sha256:30afa8f564350770373f299d2d267bff42aaba699a7ae0a3b6f378b2a8170569", - "sha256:a7a36c3c657f06bd1e3e3821b9480f2a92017d8a26e150e464ab6b97743cbc92" + "sha256:6afc461ab3f779c9c16e299fc731d775e39ea7e8e063b3053ee359ae198a15ca", + "sha256:ce1c38823eb0f927567cde5bf2e7c8ca565c7a70316139342050ce2ca74b4026" ], "markers": "python_version >= '3.5'", - "version": "==4.14.0" + "version": "==4.14.2" }, "filelock": { "hashes": [ @@ -751,10 +1173,10 @@ }, "pytz": { "hashes": [ - "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", - "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" + "sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268", + "sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd" ], - "version": "==2020.1" + "version": "==2020.4" }, "requests": { "hashes": [ @@ -781,11 +1203,11 @@ }, "sphinx": { "hashes": [ - "sha256:321d6d9b16fa381a5306e5a0b76cd48ffbc588e6340059a729c6fdd66087e0e8", - "sha256:ce6fd7ff5b215af39e2fcd44d4a321f6694b4530b6f2b2109b64d120773faea0" + "sha256:1c21e7c5481a31b531e6cbf59c3292852ccde175b504b00ce2ff0b8f4adc3649", + "sha256:3abdb2c57a65afaaa4f8573cbabd5465078eb6fd282c1e4f87f006875a7ec0c7" ], "index": "pypi", - "version": "==3.2.1" + "version": "==3.3.0" }, "sphinxcontrib-applehelp": { "hashes": [ diff --git a/src/documents/consumer.py b/src/documents/consumer.py index f61d11136..3a73fc0cd 100755 --- a/src/documents/consumer.py +++ b/src/documents/consumer.py @@ -5,6 +5,8 @@ import os import re import uuid +from asgiref.sync import async_to_sync +from channels.layers import get_channel_layer from django.conf import settings from django.db import transaction from django.utils import timezone @@ -33,6 +35,17 @@ class Consumer: 5. Delete the document and image(s) """ + def _send_progress(self, filename, current_progress, max_progress, status, message, document_id=None): + payload = { + 'filename': os.path.basename(filename), + 'current_progress': current_progress, + 'max_progress': max_progress, + 'status': status, + 'message': message, + 'document_id': document_id + } + async_to_sync(self.channel_layer.group_send)("status_updates", {'type': 'status_update', 'data': payload}) + def __init__(self, consume=settings.CONSUMPTION_DIR, scratch=settings.SCRATCH_DIR): @@ -44,6 +57,8 @@ class Consumer: self.classifier = DocumentClassifier() + self.channel_layer = get_channel_layer() + os.makedirs(self.scratch, exist_ok=True) self.storage_type = Document.STORAGE_TYPE_UNENCRYPTED @@ -60,7 +75,6 @@ class Consumer: raise ConsumerError( "Consumption directory {} does not exist".format(self.consume)) - def log(self, level, message): getattr(self.logger, level)(message, extra={ "group": self.logging_group @@ -88,6 +102,7 @@ class Consumer: self.log("info", "Consuming {}".format(doc)) + parser_class = get_parser_class(doc) if not parser_class: self.log( @@ -96,6 +111,7 @@ class Consumer: else: self.log("info", "Parser: {}".format(parser_class.__name__)) + self._send_progress(file, 0, 100, 'WORKING', 'Consumption started') document_consumption_started.send( sender=self.__class__, @@ -103,20 +119,37 @@ class Consumer: logging_group=self.logging_group ) - document_parser = parser_class(doc, self.logging_group) + def progress_callback(current_progress, max_progress, message): + # recalculate progress to be within 20 and 80 + p = int((current_progress / max_progress) * 60 + 20) + self._send_progress(file, p, 100, "WORKING", message) + + document_parser = parser_class(doc, self.logging_group, progress_callback) try: self.log("info", "Generating thumbnail for {}...".format(doc)) + self._send_progress(file, 10, 100, 'WORKING', + 'Generating thumbnail...') thumbnail = document_parser.get_optimised_thumbnail() + self._send_progress(file, 20, 100, 'WORKING', + 'Getting text from document...') + text = document_parser.get_text() + self._send_progress(file, 80, 100, 'WORKING', + 'Getting date from document...') date = document_parser.get_date() + self._send_progress(file, 85, 100, 'WORKING', + 'Storing the document...') document = self._store( - document_parser.get_text(), + text, doc, thumbnail, date ) except ParseError as e: self.log("fatal", "PARSE FAILURE for {}: {}".format(doc, e)) + self._send_progress(file, 100, 100, 'FAILED', + "Failed: {}".format(e)) + document_parser.cleanup() return False else: @@ -136,12 +169,17 @@ class Consumer: except (FileNotFoundError, IncompatibleClassifierVersionError) as e: logging.getLogger(__name__).warning("Cannot classify documents: {}.".format(e)) + self._send_progress(file, 90, 100, 'WORKING', + 'Performing post-consumption tasks...') + document_consumption_finished.send( sender=self.__class__, document=document, logging_group=self.logging_group, classifier=classifier ) + self._send_progress(file, 100, 100, 'SUCCESS', + 'Finished.', document.id) return True def _store(self, text, doc, thumbnail, date): diff --git a/src/documents/parsers.py b/src/documents/parsers.py index 0cbd13987..7976f4739 100644 --- a/src/documents/parsers.py +++ b/src/documents/parsers.py @@ -106,11 +106,12 @@ class DocumentParser: `paperless_tesseract.parsers` for inspiration. """ - def __init__(self, path, logging_group): + def __init__(self, path, logging_group, progress_callback): self.document_path = path self.tempdir = tempfile.mkdtemp(prefix="paperless-", dir=settings.SCRATCH_DIR) self.logger = logging.getLogger(__name__) self.logging_group = logging_group + self.progress_callback = progress_callback def get_thumbnail(self): """ diff --git a/src/paperless/asgi.py b/src/paperless/asgi.py new file mode 100644 index 000000000..9c3d17b1b --- /dev/null +++ b/src/paperless/asgi.py @@ -0,0 +1,37 @@ +import json +import os + +from asgiref.sync import async_to_sync +from channels.auth import AuthMiddlewareStack +from channels.generic.websocket import WebsocketConsumer +from channels.routing import ProtocolTypeRouter, URLRouter +from django.core.asgi import get_asgi_application +from django.urls import re_path + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'paperless.settings') + + +class StatusConsumer(WebsocketConsumer): + def connect(self): + self.accept() + async_to_sync(self.channel_layer.group_add)('status_updates', self.channel_name) + + def disconnect(self, close_code): + async_to_sync(self.channel_layer.group_discard)('status_updates', self.channel_name) + + def status_update(self, event): + self.send(json.dumps(event['data'])) + + +websocket_urlpatterns = [ + re_path(r'ws/status/$', StatusConsumer.as_asgi()), +] + +application = ProtocolTypeRouter({ + "http": get_asgi_application(), + "websocket": AuthMiddlewareStack( + URLRouter( + websocket_urlpatterns + ) + ), +}) diff --git a/src/paperless/settings.py b/src/paperless/settings.py index bb71e4764..c9db3e4b1 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -69,6 +69,8 @@ INSTALLED_APPS = [ "rest_framework.authtoken", "django_filters", + "channels", + ] REST_FRAMEWORK = { @@ -98,6 +100,7 @@ LOGIN_URL = "admin:login" FORCE_SCRIPT_NAME = os.getenv("PAPERLESS_FORCE_SCRIPT_NAME") WSGI_APPLICATION = 'paperless.wsgi.application' +ASGI_APPLICATION = "paperless.asgi.application" STATIC_URL = os.getenv("PAPERLESS_STATIC_URL", "/static/") @@ -299,3 +302,12 @@ FILENAME_DATE_ORDER = os.getenv("PAPERLESS_FILENAME_DATE_ORDER") FILENAME_PARSE_TRANSFORMS = [] for t in json.loads(os.getenv("PAPERLESS_FILENAME_PARSE_TRANSFORMS", "[]")): FILENAME_PARSE_TRANSFORMS.append((re.compile(t["pattern"]), t["repl"])) + +CHANNEL_LAYERS = { + "default": { + "BACKEND": "channels_redis.core.RedisChannelLayer", + "CONFIG": { + "hosts": [("127.0.0.1", 6379)], + }, + }, +} diff --git a/src/paperless_tesseract/parsers.py b/src/paperless_tesseract/parsers.py index befc9bcd7..535ee501c 100644 --- a/src/paperless_tesseract/parsers.py +++ b/src/paperless_tesseract/parsers.py @@ -27,8 +27,8 @@ class RasterisedDocumentParser(DocumentParser): image, whether it's a PDF, or other graphical format (JPEG, TIFF, etc.) """ - def __init__(self, path, logging_group): - super().__init__(path, logging_group) + def __init__(self, path, logging_group, progress_callback): + super().__init__(path, logging_group, progress_callback) self._text = None def get_thumbnail(self): @@ -91,6 +91,7 @@ class RasterisedDocumentParser(DocumentParser): self._text = get_text_from_pdf(self.document_path) return self._text + self.progress_callback(0,1,"Making greyscale images.") images = self._get_greyscale() if not images: @@ -100,8 +101,10 @@ class RasterisedDocumentParser(DocumentParser): sample_page_index = int(len(images) / 2) self.log("info", "Attempting language detection on page {} of {}...".format(sample_page_index+1, len(images))) + self.progress_callback(0.4, 1, "Language Detection.") sample_page_text = self._ocr([images[sample_page_index]], settings.OCR_LANGUAGE)[0] guessed_language = self._guess_language(sample_page_text) + self.progress_callback(0.6, 1, "OCR all the pages.") if not guessed_language or guessed_language not in ISO639: self.log("warning", "Language detection failed.") @@ -117,7 +120,7 @@ class RasterisedDocumentParser(DocumentParser): else: self.log("info", "Detected language: {}".format(guessed_language)) - ocr_pages = self._ocr(images, ISO639[guessed_language]) + ocr_pages = self._ocr(images, ISO639[guessed_language], report_progress=True) self.log("info", "OCR completed.") self._text = strip_excess_whitespace(" ".join(ocr_pages)) @@ -151,6 +154,8 @@ class RasterisedDocumentParser(DocumentParser): self.log("info", "Running unpaper on {} pages...".format(len(pnms))) + self.progress_callback(0.2,1, "Running unpaper on {} pages...".format(len(pnms))) + # Run unpaper in parallel on converted images with Pool(processes=settings.OCR_THREADS) as pool: pnms = pool.map(run_unpaper, pnms) @@ -165,11 +170,16 @@ class RasterisedDocumentParser(DocumentParser): self.log('debug', "Language detection failed with: {}".format(e)) return None - def _ocr(self, imgs, lang): + def _ocr(self, imgs, lang, report_progress=False): self.log("info", "Performing OCR on {} page(s) with language {}".format(len(imgs), lang)) + r = [] with Pool(processes=settings.OCR_THREADS) as pool: - r = pool.map(image_to_string, itertools.product(imgs, [lang])) - return r + # r = pool.map(image_to_string, itertools.product(imgs, [lang])) + for i, page in enumerate(pool.imap(image_to_string, itertools.product(imgs, [lang]))): + if report_progress: + self.progress_callback(0.6 + (i / len(imgs)) * 0.4, 1, "OCR'ed {} pages".format(i+1)) + r += [page] + return r def _complete_ocr_default_language(self, images, sample_page_index, sample_page): """ @@ -182,14 +192,13 @@ class RasterisedDocumentParser(DocumentParser): del images_copy[sample_page_index] if images_copy: self.log('info', 'Continuing ocr with default language.') - ocr_pages = self._ocr(images_copy, settings.OCR_LANGUAGE) + ocr_pages = self._ocr(images_copy, settings.OCR_LANGUAGE, report_progress=True) ocr_pages.insert(sample_page_index, sample_page) return ocr_pages else: return [sample_page] - def strip_excess_whitespace(text): collapsed_spaces = re.sub(r"([^\S\r\n]+)", " ", text) no_leading_whitespace = re.sub( From 036f11acaae7368b322ce62c31d85e3ebbd35ee9 Mon Sep 17 00:00:00 2001 From: Jonas Winkler Date: Sat, 7 Nov 2020 12:05:15 +0100 Subject: [PATCH 0002/1708] better toasts, better dashboard, first implementation of consumer status --- src-ui/src/app/app.component.ts | 35 ++++++++- src-ui/src/app/app.module.ts | 10 ++- .../edit-dialog/edit-dialog.component.ts | 4 +- .../common/toasts/toasts.component.html | 5 +- .../dashboard/dashboard.component.html | 56 ++------------- .../dashboard/dashboard.component.ts | 53 +------------- .../consumer-status-widget.component.css | 0 .../consumer-status-widget.component.html | 10 +++ .../consumer-status-widget.component.spec.ts | 25 +++++++ .../consumer-status-widget.component.ts | 35 +++++++++ .../file-upload-widget.component.css | 0 .../file-upload-widget.component.html | 11 +++ .../file-upload-widget.component.spec.ts | 25 +++++++ .../file-upload-widget.component.ts | 44 ++++++++++++ .../saved-view-widget.component.css | 0 .../saved-view-widget.component.html | 16 +++++ .../saved-view-widget.component.spec.ts | 25 +++++++ .../saved-view-widget.component.ts | 41 +++++++++++ .../statistics-widget.component.css | 0 .../statistics-widget.component.html | 3 + .../statistics-widget.component.spec.ts | 25 +++++++ .../statistics-widget.component.ts | 32 +++++++++ .../app/components/login/login.component.ts | 4 +- src-ui/src/app/services/auth.interceptor.ts | 2 +- .../services/consumer-status.service.spec.ts | 16 +++++ .../app/services/consumer-status.service.ts | 71 +++++++++++++++++++ src-ui/src/app/services/toast.service.ts | 33 ++++----- src-ui/src/environments/environment.ts | 3 +- 28 files changed, 450 insertions(+), 134 deletions(-) create mode 100644 src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.css create mode 100644 src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.html create mode 100644 src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.spec.ts create mode 100644 src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.ts create mode 100644 src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.css create mode 100644 src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.html create mode 100644 src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.spec.ts create mode 100644 src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.ts create mode 100644 src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.css create mode 100644 src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html create mode 100644 src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.spec.ts create mode 100644 src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts create mode 100644 src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.css create mode 100644 src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html create mode 100644 src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.spec.ts create mode 100644 src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.ts create mode 100644 src-ui/src/app/services/consumer-status.service.spec.ts create mode 100644 src-ui/src/app/services/consumer-status.service.ts diff --git a/src-ui/src/app/app.component.ts b/src-ui/src/app/app.component.ts index a6cd8bebe..5809968e8 100644 --- a/src-ui/src/app/app.component.ts +++ b/src-ui/src/app/app.component.ts @@ -1,14 +1,43 @@ -import { Component } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { Subscription } from 'rxjs'; +import { ConsumerStatusService } from './services/consumer-status.service'; +import { Toast, ToastService } from './services/toast.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) -export class AppComponent { +export class AppComponent implements OnInit, OnDestroy { + + successSubscription: Subscription; + failedSubscription: Subscription; - constructor () { + constructor ( private consumerStatusService: ConsumerStatusService, private toastService: ToastService, private router: Router ) { } + ngOnDestroy(): void { + this.consumerStatusService.disconnect() + this.successSubscription.unsubscribe() + this.failedSubscription.unsubscribe() + } + + ngOnInit(): void { + this.consumerStatusService.connect() + + this.successSubscription = this.consumerStatusService.onDocumentConsumptionFinished().subscribe(status => { + this.toastService.showToast({title: "Document added", content: `Document ${status.filename} was added to paperless.`, actionName: "Open document", action: () => { + this.router.navigate(['documents', status.document_id]) + }}) + }) + + this.failedSubscription = this.consumerStatusService.onDocumentConsumptionFailed().subscribe(status => { + this.toastService.showError(`Could not consume ${status.filename}: ${status.message}`) + }) + + } + + } diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index e10bdbd0c..5cc59d567 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -40,6 +40,10 @@ import { SaveViewConfigDialogComponent } from './components/document-list/save-v import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { DateTimeComponent } from './components/common/input/date-time/date-time.component'; import { TagsComponent } from './components/common/input/tags/tags.component'; +import { SavedViewWidgetComponent } from './components/dashboard/widgets/saved-view-widget/saved-view-widget.component'; +import { ConsumerStatusWidgetComponent } from './components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component'; +import { StatisticsWidgetComponent } from './components/dashboard/widgets/statistics-widget/statistics-widget.component'; +import { FileUploadWidgetComponent } from './components/dashboard/widgets/file-upload-widget/file-upload-widget.component'; @NgModule({ declarations: [ @@ -73,7 +77,11 @@ import { TagsComponent } from './components/common/input/tags/tags.component'; CheckComponent, SaveViewConfigDialogComponent, DateTimeComponent, - TagsComponent + TagsComponent, + ConsumerStatusWidgetComponent, + SavedViewWidgetComponent, + StatisticsWidgetComponent, + FileUploadWidgetComponent ], imports: [ BrowserModule, diff --git a/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.ts index ba0d90847..04eb1f250 100644 --- a/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.ts +++ b/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.ts @@ -5,7 +5,7 @@ import { Observable } from 'rxjs'; import { MATCHING_ALGORITHMS } from 'src/app/data/matching-model'; import { ObjectWithId } from 'src/app/data/object-with-id'; import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'; -import { Toast, ToastService } from 'src/app/services/toast.service'; +import { ToastService } from 'src/app/services/toast.service'; @Directive() export abstract class EditDialogComponent implements OnInit { @@ -66,7 +66,7 @@ export abstract class EditDialogComponent implements OnI this.activeModal.close() this.success.emit(result) }, error => { - this.toastService.showToast(Toast.makeError(`Could not save ${this.entityName}: ${error.error.name}`)) + this.toastService.showError(`Could not save ${this.entityName}: ${error.error.name}`) }) } diff --git a/src-ui/src/app/components/common/toasts/toasts.component.html b/src-ui/src/app/components/common/toasts/toasts.component.html index 04aa15a67..4e920877e 100644 --- a/src-ui/src/app/components/common/toasts/toasts.component.html +++ b/src-ui/src/app/components/common/toasts/toasts.component.html @@ -1,7 +1,8 @@ - {{toast.content}} +

{{toast.content}}

+

\ No newline at end of file diff --git a/src-ui/src/app/components/dashboard/dashboard.component.html b/src-ui/src/app/components/dashboard/dashboard.component.html index 694b431c4..4b732c9e8 100644 --- a/src-ui/src/app/components/dashboard/dashboard.component.html +++ b/src-ui/src/app/components/dashboard/dashboard.component.html @@ -6,59 +6,11 @@
- -

{{v.viewConfig.title}}

- - - - - - - - - - - - - -
Date createdDocument
{{doc.created | date}}{{doc.title}} -
- -
- -

Saved views

-

This space is reserved to display your saved views. Go to your documents and save a view to have it displayed here!

-
- +
-

Statistics

-

Documents in inbox: {{statistics.documents_inbox}}

-

Total documents: {{statistics.documents_total}}

-

Upload new Document

-
- - - -
-
Document conumser status
-

This is what it might look like in the future.

-
-
-

Filename.pdf: Running tesseract on page 4/8...

-

-
-
-
-
-

Filename2.pdf: Completed.

-

-
-
+ + +
diff --git a/src-ui/src/app/components/dashboard/dashboard.component.ts b/src-ui/src/app/components/dashboard/dashboard.component.ts index f8d5fb0ae..a9c72e496 100644 --- a/src-ui/src/app/components/dashboard/dashboard.component.ts +++ b/src-ui/src/app/components/dashboard/dashboard.component.ts @@ -4,14 +4,9 @@ import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop'; import { Observable } from 'rxjs'; import { DocumentService } from 'src/app/services/rest/document.service'; import { SavedViewConfigService } from 'src/app/services/saved-view-config.service'; -import { Toast, ToastService } from 'src/app/services/toast.service'; +import { ToastService } from 'src/app/services/toast.service'; import { environment } from 'src/environments/environment'; -export interface Statistics { - documents_total?: number - documents_inbox?: number -} - @Component({ selector: 'app-dashboard', templateUrl: './dashboard.component.html', @@ -19,53 +14,9 @@ export interface Statistics { }) export class DashboardComponent implements OnInit { - constructor(private documentService: DocumentService, private toastService: ToastService, - public savedViewConfigService: SavedViewConfigService, private http: HttpClient) { } - - - savedDashboardViews = [] - statistics: Statistics = {} + constructor(public savedViewConfigService: SavedViewConfigService) { } ngOnInit(): void { - this.savedViewConfigService.getDashboardConfigs().forEach(config => { - this.documentService.list(1,10,config.sortField,config.sortDirection,config.filterRules).subscribe(result => { - this.savedDashboardViews.push({viewConfig: config, documents: result.results}) - }) - }) - this.getStatistics().subscribe(statistics => { - this.statistics = statistics - }) } - getStatistics(): Observable { - return this.http.get(`${environment.apiBaseUrl}statistics/`) - } - - - public fileOver(event){ - console.log(event); - } - - public fileLeave(event){ - console.log(event); - } - - public dropped(files: NgxFileDropEntry[]) { - for (const droppedFile of files) { - if (droppedFile.fileEntry.isFile) { - const fileEntry = droppedFile.fileEntry as FileSystemFileEntry; - console.log(fileEntry) - fileEntry.file((file: File) => { - console.log(file) - const formData = new FormData() - formData.append('document', file, file.name) - this.documentService.uploadDocument(formData).subscribe(result => { - this.toastService.showToast(Toast.make("Information", "The document has been uploaded and will be processed by the consumer shortly.")) - }, error => { - this.toastService.showToast(Toast.makeError("An error has occured while uploading the document. Sorry!")) - }) - }); - } - } - } } diff --git a/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.css b/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.html b/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.html new file mode 100644 index 000000000..9f6aa3b87 --- /dev/null +++ b/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.html @@ -0,0 +1,10 @@ +

Document consumer status

+ +
+
{{s.filename}}: {{s.message}}
+ +
+ + +
+
\ No newline at end of file diff --git a/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.spec.ts b/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.spec.ts new file mode 100644 index 000000000..02aa088f5 --- /dev/null +++ b/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ConsumerStatusWidgetComponent } from './consumer-status-widget.component'; + +describe('ConsumerStatusWidgetComponent', () => { + let component: ConsumerStatusWidgetComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ConsumerStatusWidgetComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ConsumerStatusWidgetComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.ts b/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.ts new file mode 100644 index 000000000..0c4e35682 --- /dev/null +++ b/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.ts @@ -0,0 +1,35 @@ +import { Component, OnInit } from '@angular/core'; +import { ConsumerStatusService, FileStatus } from 'src/app/services/consumer-status.service'; + +@Component({ + selector: 'app-consumer-status-widget', + templateUrl: './consumer-status-widget.component.html', + styleUrls: ['./consumer-status-widget.component.css'] +}) +export class ConsumerStatusWidgetComponent implements OnInit { + + constructor(private consumerStatusService: ConsumerStatusService) { } + + ngOnInit(): void { + } + + getStatus() { + return this.consumerStatusService.consumerStatus + } + + isFinished(status: FileStatus) { + return status.status == "FAILED" || status.status == "SUCCESS" + } + + getType(status) { + switch (status) { + case "WORKING": return "primary" + case "FAILED": return "danger" + case "SUCCESS": return "success" + } + } + + dismiss(status: FileStatus) { + this.consumerStatusService.dismiss(status) + } +} diff --git a/src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.css b/src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.html b/src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.html new file mode 100644 index 000000000..0c5ea634a --- /dev/null +++ b/src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.html @@ -0,0 +1,11 @@ +

Upload new Document

+
+ + + +
\ No newline at end of file diff --git a/src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.spec.ts b/src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.spec.ts new file mode 100644 index 000000000..847f5288b --- /dev/null +++ b/src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FileUploadWidgetComponent } from './file-upload-widget.component'; + +describe('FileUploadWidgetComponent', () => { + let component: FileUploadWidgetComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ FileUploadWidgetComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FileUploadWidgetComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.ts b/src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.ts new file mode 100644 index 000000000..5d4bac936 --- /dev/null +++ b/src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.ts @@ -0,0 +1,44 @@ +import { Component, OnInit } from '@angular/core'; +import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop'; +import { DocumentService } from 'src/app/services/rest/document.service'; +import { ToastService } from 'src/app/services/toast.service'; + +@Component({ + selector: 'app-file-upload-widget', + templateUrl: './file-upload-widget.component.html', + styleUrls: ['./file-upload-widget.component.css'] +}) +export class FileUploadWidgetComponent implements OnInit { + + constructor(private documentService: DocumentService, private toastService: ToastService) { } + + ngOnInit(): void { + } + + public fileOver(event){ + console.log(event); + } + + public fileLeave(event){ + console.log(event); + } + + public dropped(files: NgxFileDropEntry[]) { + for (const droppedFile of files) { + if (droppedFile.fileEntry.isFile) { + const fileEntry = droppedFile.fileEntry as FileSystemFileEntry; + console.log(fileEntry) + fileEntry.file((file: File) => { + console.log(file) + const formData = new FormData() + formData.append('document', file, file.name) + this.documentService.uploadDocument(formData).subscribe(result => { + this.toastService.showInfo("The document has been uploaded and will be processed by the consumer shortly.") + }, error => { + this.toastService.showError("An error has occured while uploading the document. Sorry!") + }) + }); + } + } + } +} diff --git a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.css b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html new file mode 100644 index 000000000..110464641 --- /dev/null +++ b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html @@ -0,0 +1,16 @@ +

{{viewConfig.title}}

+ + + + + + + + + + + + + +
Date createdDocument
{{doc.created | date}}{{doc.title}} +
\ No newline at end of file diff --git a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.spec.ts b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.spec.ts new file mode 100644 index 000000000..f0095b618 --- /dev/null +++ b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SavedViewWidgetComponent } from './saved-view-widget.component'; + +describe('SavedViewWidgetComponent', () => { + let component: SavedViewWidgetComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SavedViewWidgetComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SavedViewWidgetComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts new file mode 100644 index 000000000..eb2d53aee --- /dev/null +++ b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts @@ -0,0 +1,41 @@ +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { PaperlessDocument } from 'src/app/data/paperless-document'; +import { SavedViewConfig } from 'src/app/data/saved-view-config'; +import { ConsumerStatusService } from 'src/app/services/consumer-status.service'; +import { DocumentService } from 'src/app/services/rest/document.service'; + +@Component({ + selector: 'app-saved-view-widget', + templateUrl: './saved-view-widget.component.html', + styleUrls: ['./saved-view-widget.component.css'] +}) +export class SavedViewWidgetComponent implements OnInit, OnDestroy { + + constructor(private documentService: DocumentService, private consumerStatusService: ConsumerStatusService) { } + + @Input() + viewConfig: SavedViewConfig + + documents: PaperlessDocument[] + + subscription: Subscription + + ngOnInit(): void { + this.reload() + this.subscription = this.consumerStatusService.onDocumentConsumptionFinished().subscribe(status => { + this.reload() + }) + } + + ngOnDestroy(): void { + this.subscription.unsubscribe() + } + + reload() { + this.documentService.list(1,10,this.viewConfig.sortField,this.viewConfig.sortDirection,this.viewConfig.filterRules).subscribe(result => { + this.documents = result.results + }) + } + +} diff --git a/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.css b/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html b/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html new file mode 100644 index 000000000..2f89a2b34 --- /dev/null +++ b/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html @@ -0,0 +1,3 @@ +

Statistics

+

Documents in inbox: {{statistics.documents_inbox}}

+

Total documents: {{statistics.documents_total}}

\ No newline at end of file diff --git a/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.spec.ts b/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.spec.ts new file mode 100644 index 000000000..e8e44ca54 --- /dev/null +++ b/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { StatisticsWidgetComponent } from './statistics-widget.component'; + +describe('StatisticsWidgetComponent', () => { + let component: StatisticsWidgetComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ StatisticsWidgetComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(StatisticsWidgetComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.ts b/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.ts new file mode 100644 index 000000000..4efb03895 --- /dev/null +++ b/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.ts @@ -0,0 +1,32 @@ +import { HttpClient } from '@angular/common/http'; +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { environment } from 'src/environments/environment'; + +export interface Statistics { + documents_total?: number + documents_inbox?: number +} + +@Component({ + selector: 'app-statistics-widget', + templateUrl: './statistics-widget.component.html', + styleUrls: ['./statistics-widget.component.css'] +}) +export class StatisticsWidgetComponent implements OnInit { + + constructor(private http: HttpClient) { } + + statistics: Statistics = {} + + ngOnInit(): void { + this.getStatistics().subscribe(statistics => { + this.statistics = statistics + }) + } + + getStatistics(): Observable { + return this.http.get(`${environment.apiBaseUrl}statistics/`) + } + +} diff --git a/src-ui/src/app/components/login/login.component.ts b/src-ui/src/app/components/login/login.component.ts index e74dcfb7f..a241543c7 100644 --- a/src-ui/src/app/components/login/login.component.ts +++ b/src-ui/src/app/components/login/login.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; import { Router } from '@angular/router'; import { AuthService } from 'src/app/services/auth.service'; -import { Toast, ToastService } from 'src/app/services/toast.service'; +import { ToastService } from 'src/app/services/toast.service'; @Component({ selector: 'app-login', @@ -26,7 +26,7 @@ export class LoginComponent implements OnInit { this.auth.login(this.loginForm.value.username, this.loginForm.value.password, this.loginForm.value.rememberMe).subscribe(result => { this.router.navigate(['']) }, (error) => { - this.toastService.showToast(Toast.makeError("Unable to log in with provided credentials.")) + this.toastService.showError("Unable to log in with provided credentials.") } ) } diff --git a/src-ui/src/app/services/auth.interceptor.ts b/src-ui/src/app/services/auth.interceptor.ts index 704b558ac..37d9e7906 100644 --- a/src-ui/src/app/services/auth.interceptor.ts +++ b/src-ui/src/app/services/auth.interceptor.ts @@ -28,7 +28,7 @@ export class AuthInterceptor implements HttpInterceptor { catchError((error: HttpErrorResponse) => { if (error.status == 401 && this.authService.isAuthenticated()) { this.authService.logout() - this.toastService.showToast(Toast.makeError("Your session has expired. Please log in again.")) + this.toastService.showError("Your session has expired. Please log in again.") } return throwError(error) }) diff --git a/src-ui/src/app/services/consumer-status.service.spec.ts b/src-ui/src/app/services/consumer-status.service.spec.ts new file mode 100644 index 000000000..d19f455e2 --- /dev/null +++ b/src-ui/src/app/services/consumer-status.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ConsumerStatusService } from './consumer-status.service'; + +describe('ConsumerStatusService', () => { + let service: ConsumerStatusService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ConsumerStatusService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src-ui/src/app/services/consumer-status.service.ts b/src-ui/src/app/services/consumer-status.service.ts new file mode 100644 index 000000000..070420b0f --- /dev/null +++ b/src-ui/src/app/services/consumer-status.service.ts @@ -0,0 +1,71 @@ +import { Injectable } from '@angular/core'; +import { Subject } from 'rxjs'; + +export interface FileStatus { + filename?: string + current_progress?: number + max_progress?: number + status?: string + message?: string + document_id?: number +} + +@Injectable({ + providedIn: 'root' +}) +export class ConsumerStatusService { + + constructor() { } + + private statusWebSocked: WebSocket + + consumerStatus: FileStatus[] = [] + private documentConsumptionFinishedSubject = new Subject() + private documentConsumptionFailedSubject = new Subject() + + connect() { + this.disconnect() + this.statusWebSocked = new WebSocket("ws://localhost:8000/ws/status/"); + this.statusWebSocked.onmessage = (ev) => { + let statusUpdate: FileStatus = JSON.parse(ev['data']) + + let index = this.consumerStatus.findIndex(fs => fs.filename == statusUpdate.filename) + if (index > -1) { + this.consumerStatus[index] = statusUpdate + } else { + this.consumerStatus.push(statusUpdate) + } + + if (statusUpdate.status == "SUCCESS") { + this.documentConsumptionFinishedSubject.next(statusUpdate) + } + if (statusUpdate.status == "FAILED") { + this.documentConsumptionFailedSubject.next(statusUpdate) + } + } + } + + disconnect() { + if (this.statusWebSocked) { + this.statusWebSocked.close() + this.statusWebSocked = null + } + } + + dismiss(status: FileStatus) { + let index = this.consumerStatus.findIndex(s => s.filename == status.filename) + + if (index > -1) { + this.consumerStatus.splice(index, 1) + } + } + + onDocumentConsumptionFinished() { + return this.documentConsumptionFinishedSubject + } + + onDocumentConsumptionFailed() { + return this.documentConsumptionFailedSubject + } + +} diff --git a/src-ui/src/app/services/toast.service.ts b/src-ui/src/app/services/toast.service.ts index a3ce060a9..d5781139e 100644 --- a/src-ui/src/app/services/toast.service.ts +++ b/src-ui/src/app/services/toast.service.ts @@ -1,30 +1,17 @@ import { Injectable } from '@angular/core'; import { Subject, zip } from 'rxjs'; -export class Toast { - - static make(title: string, content: string, classname?: string, delay?: number): Toast { - let t = new Toast() - t.title = title - t.content = content - t.classname = classname - if (delay) { - t.delay = delay - } - return t - } - - static makeError(content: string) { - return Toast.make("Error", content, null, 10000) - } +export interface Toast { title: string - classname: string - content: string - delay: number = 5000 + delay?: number + + action?: any + + actionName?: string } @@ -44,6 +31,14 @@ export class ToastService { this.toastsSubject.next(this.toasts) } + showInfo(message: string) { + this.showToast({title: "Information", content: message, delay: 5000}) + } + + showError(message: string) { + this.showToast({title: "Error", content: message, delay: 10000}) + } + closeToast(toast: Toast) { let index = this.toasts.findIndex(t => t == toast) if (index > -1) { diff --git a/src-ui/src/environments/environment.ts b/src-ui/src/environments/environment.ts index a0877d69f..c945a0364 100644 --- a/src-ui/src/environments/environment.ts +++ b/src-ui/src/environments/environment.ts @@ -4,7 +4,8 @@ export const environment = { production: false, - apiBaseUrl: "http://localhost:8000/api/" + apiBaseUrl: "http://localhost:8000/api/", + wsBaseUrl: "ws://localhost:8000/ws/" }; /* From b0465e65c3ac85b58e3cb6c3daa2f755ee386c98 Mon Sep 17 00:00:00 2001 From: Jonas Winkler Date: Sat, 7 Nov 2020 12:10:53 +0100 Subject: [PATCH 0003/1708] nicer status --- .../consumer-status-widget.component.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.html b/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.html index 9f6aa3b87..d6559e184 100644 --- a/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.html +++ b/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.html @@ -1,10 +1,10 @@

Document consumer status

-
+
{{s.filename}}: {{s.message}}
-
- +
+
\ No newline at end of file From ae8a048ea6a570e9a15ba67d954297582b4ddeca Mon Sep 17 00:00:00 2001 From: Jonas Winkler Date: Sat, 7 Nov 2020 12:47:17 +0100 Subject: [PATCH 0004/1708] fixed up the docker --- Pipfile | 1 + Pipfile.lock | 4 ++-- scripts/supervisord.conf | 4 ++-- src/paperless/asgi.py | 30 ++++++++---------------------- src/paperless/consumers.py | 16 ++++++++++++++++ src/paperless/settings.py | 2 +- src/paperless/urls.py | 8 +++++++- 7 files changed, 37 insertions(+), 28 deletions(-) create mode 100644 src/paperless/consumers.py diff --git a/Pipfile b/Pipfile index d526ae252..09de36334 100644 --- a/Pipfile +++ b/Pipfile @@ -28,6 +28,7 @@ django-extensions = "" watchdog = "*" channels = "~=3.0" channels-redis = "*" +daphne = "~=3.0" [dev-packages] coveralls = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 642e38214..21b4becad 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "192d7419b844e6bb81fed793e7766b2ba15f2a016af1a33fc73cf09e12de5fb7" + "sha256": "66530e76de7948d8123529eff0b150926aa46a410da8b31e12b6d468e5996e7a" }, "pipfile-spec": 6, "requires": {}, @@ -157,7 +157,7 @@ "sha256:60856f7efa0b1e1b969efa074e8698bd09de4713ecc06e6a4d19d04c66c4a3bd", "sha256:b43e70d74ff832a634ff6c92badd208824e4530e08b340116517e5aad0aca774" ], - "markers": "python_version >= '3.6'", + "index": "pypi", "version": "==3.0.0" }, "dateparser": { diff --git a/scripts/supervisord.conf b/scripts/supervisord.conf index d3ff288de..cb6fd1650 100644 --- a/scripts/supervisord.conf +++ b/scripts/supervisord.conf @@ -6,8 +6,8 @@ logfile_maxbytes=50MB ; max main logfile bytes b4 rotation; default 50MB logfile_backups=10 ; # of main logfile backups; 0 means none, default 10 loglevel=info ; log level; default info; others: debug,warn,trace -[program:gunicorn] -command=gunicorn -c /usr/src/paperless/gunicorn.conf.py -b 0.0.0.0:8000 paperless.wsgi +[program:daphne] +command=daphne -b 0.0.0.0 -p 8000 paperless.asgi:application user=paperless stdout_logfile=/dev/stdout diff --git a/src/paperless/asgi.py b/src/paperless/asgi.py index 9c3d17b1b..45565c68a 100644 --- a/src/paperless/asgi.py +++ b/src/paperless/asgi.py @@ -1,31 +1,17 @@ -import json import os -from asgiref.sync import async_to_sync -from channels.auth import AuthMiddlewareStack -from channels.generic.websocket import WebsocketConsumer -from channels.routing import ProtocolTypeRouter, URLRouter from django.core.asgi import get_asgi_application -from django.urls import re_path +# Fetch Django ASGI application early to ensure AppRegistry is populated +# before importing consumers and AuthMiddlewareStack that may import ORM +# models. -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'paperless.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "paperless.settings") +django_asgi_app = get_asgi_application() +from channels.auth import AuthMiddlewareStack +from channels.routing import ProtocolTypeRouter, URLRouter -class StatusConsumer(WebsocketConsumer): - def connect(self): - self.accept() - async_to_sync(self.channel_layer.group_add)('status_updates', self.channel_name) - - def disconnect(self, close_code): - async_to_sync(self.channel_layer.group_discard)('status_updates', self.channel_name) - - def status_update(self, event): - self.send(json.dumps(event['data'])) - - -websocket_urlpatterns = [ - re_path(r'ws/status/$', StatusConsumer.as_asgi()), -] +from paperless.urls import websocket_urlpatterns application = ProtocolTypeRouter({ "http": get_asgi_application(), diff --git a/src/paperless/consumers.py b/src/paperless/consumers.py new file mode 100644 index 000000000..fbb7b72d0 --- /dev/null +++ b/src/paperless/consumers.py @@ -0,0 +1,16 @@ +import json + +from asgiref.sync import async_to_sync +from channels.generic.websocket import WebsocketConsumer + + +class StatusConsumer(WebsocketConsumer): + def connect(self): + self.accept() + async_to_sync(self.channel_layer.group_add)('status_updates', self.channel_name) + + def disconnect(self, close_code): + async_to_sync(self.channel_layer.group_discard)('status_updates', self.channel_name) + + def status_update(self, event): + self.send(json.dumps(event['data'])) diff --git a/src/paperless/settings.py b/src/paperless/settings.py index c9db3e4b1..7ef132bff 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -307,7 +307,7 @@ CHANNEL_LAYERS = { "default": { "BACKEND": "channels_redis.core.RedisChannelLayer", "CONFIG": { - "hosts": [("127.0.0.1", 6379)], + "hosts": [("broker", 6379)], }, }, } diff --git a/src/paperless/urls.py b/src/paperless/urls.py index 43ba5eb49..4416bc8d5 100755 --- a/src/paperless/urls.py +++ b/src/paperless/urls.py @@ -1,11 +1,12 @@ from django.conf.urls import include, url from django.contrib import admin -from django.urls import path +from django.urls import path, re_path from django.views.decorators.csrf import csrf_exempt from django.views.generic import RedirectView from rest_framework.authtoken import views from rest_framework.routers import DefaultRouter +from paperless.consumers import StatusConsumer from paperless.views import FaviconView from documents.views import ( CorrespondentViewSet, @@ -65,6 +66,11 @@ urlpatterns = [ ] + +websocket_urlpatterns = [ + re_path(r'ws/status/$', StatusConsumer.as_asgi()), +] + # Text in each page's

(and above login form). admin.site.site_header = 'Paperless' # Text at the end of each page's . From 37bd4a7d0edab657488143bcc8427b5d13f1699a Mon Sep 17 00:00:00 2001 From: Jonas Winkler <jonas.winkler@jpwinkler.de> Date: Sat, 7 Nov 2020 12:56:26 +0100 Subject: [PATCH 0005/1708] added broker to compose file --- docker-compose.yml.example | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docker-compose.yml.example b/docker-compose.yml.example index 1130e26a3..3d8ed4719 100644 --- a/docker-compose.yml.example +++ b/docker-compose.yml.example @@ -1,5 +1,10 @@ version: "3.4" services: + broker: + image: redis:latest + ports: + - 6379:6379 + db: image: postgres:13 #restart: always From 9e81c82452db80ced437228f62e6713f925adb9f Mon Sep 17 00:00:00 2001 From: Jonas Winkler <jonas.winkler@jpwinkler.de> Date: Tue, 10 Nov 2020 01:26:27 +0100 Subject: [PATCH 0006/1708] Pipfile update --- Pipfile.lock | 75 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 11 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 21b4becad..932dc380e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "66530e76de7948d8123529eff0b150926aa46a410da8b31e12b6d468e5996e7a" + "sha256": "65b67f06b1ad7d6541a1f84552f1c24a0353ed03aa4c382f8638c9d112b41eda" }, "pipfile-spec": 6, "requires": {}, @@ -21,13 +21,21 @@ ], "version": "==1.3.1" }, + "arrow": { + "hashes": [ + "sha256:e098abbd9af3665aea81bdd6c869e93af4feb078e98468dd351c383af187aac5", + "sha256:ff08d10cda1d36c68657d6ad20d74fbea493d980f8b2d45344e00d6ed2bf6ed4" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.17.0" + }, "asgiref": { "hashes": [ - "sha256:a5098bc870b80e7b872bff60bb363c7f2c2c89078759f6c47b53ff8c525a152e", - "sha256:cd88907ecaec59d78e4ac00ea665b03e571cb37e3a0e37b3702af1a9e86c365a" + "sha256:5ee950735509d04eb673bd7f7120f8fa1c9e2df495394992c73234d526907e17", + "sha256:7162a3cb30ab0609f1a4c95938fd73e8604f63bdba516a7f7d64b83ff09478f0" ], "markers": "python_version >= '3.5'", - "version": "==3.3.0" + "version": "==3.3.1" }, "async-timeout": { "hashes": [ @@ -60,6 +68,13 @@ ], "version": "==20.2.0" }, + "blessed": { + "hashes": [ + "sha256:7d4914079a6e8e14fbe080dcaf14dee596a088057cdc598561080e3266123b48", + "sha256:81125aa5b84cb9dfc09ff451886f64b4b923b75c5eaf51fde9d1c48a135eb797" + ], + "version": "==1.17.11" + }, "cffi": { "hashes": [ "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d", @@ -103,11 +118,11 @@ }, "channels": { "hashes": [ - "sha256:5cdd9c6b9ee663cdf1bbb00de7cdab885a3c418f9d32a29f04b09498828020f6", - "sha256:b02e150b48704ec3607d4168402ac5c26138dd183fcdb7f2aeb965e6e19fd558" + "sha256:74db79c9eca616be69d38013b22083ab5d3f9ccda1ab5e69096b1bb7da2d9b18", + "sha256:f50a6e79757a64c1e45e95e144a2ac5f1e99ee44a0718ab182c501f5e5abd268" ], "index": "pypi", - "version": "==3.0.1" + "version": "==3.0.2" }, "channels-redis": { "hashes": [ @@ -190,7 +205,6 @@ "sha256:dc663652ac9460fd06580a973576820430c6d428720e874ae46b041fa63e0efa" ], "index": "pypi", - "markers": "python_version >= '3.5'", "version": "==3.0.9" }, "django-filter": { @@ -201,6 +215,22 @@ "index": "pypi", "version": "==2.4.0" }, + "django-picklefield": { + "hashes": [ + "sha256:15ccba592ca953b9edf9532e64640329cd47b136b7f8f10f2939caa5f9ce4287", + "sha256:3c702a54fde2d322fe5b2f39b8f78d9f655b8f77944ab26f703be6c0ed335a35" + ], + "markers": "python_version >= '3'", + "version": "==3.0.1" + }, + "django-q": { + "hashes": [ + "sha256:523d54dcf1b66152c1b658f914f00ed3b518a3432a9decd4898738ca8dbbe10f", + "sha256:7e5c5c021a15cff6807044a3aa48f5757789ccfef839d71c575f5512931a3e33" + ], + "index": "pypi", + "version": "==1.3.4" + }, "djangorestframework": { "hashes": [ "sha256:0209bafcb7b5010fdfec784034f059d512256424de2a0f084cb82b096d6dd6a7" @@ -390,6 +420,14 @@ ], "version": "==0.1.2" }, + "pathvalidate": { + "hashes": [ + "sha256:1697c8ea71ff4c48e7aa0eda72fe4581404be8f41e51a17363ef682dd6824d35", + "sha256:32d30dbacb711c16bb188b12ce7e9a46b41785f50a12f64500f747480a4b6ee3" + ], + "index": "pypi", + "version": "==2.3.0" + }, "pdftotext": { "hashes": [ "sha256:98aeb8b07a4127e1a30223bd933ef080bbd29aa88f801717ca6c5618380b8aa6" @@ -576,6 +614,14 @@ ], "version": "==2020.4" }, + "redis": { + "hashes": [ + "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2", + "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24" + ], + "index": "pypi", + "version": "==3.5.3" + }, "regex": { "hashes": [ "sha256:03855ee22980c3e4863dc84c42d6d2901133362db5daf4c36b710dd895d78f0a", @@ -762,6 +808,13 @@ "index": "pypi", "version": "==0.10.3" }, + "wcwidth": { + "hashes": [ + "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784", + "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83" + ], + "version": "==0.2.5" + }, "whitenoise": { "hashes": [ "sha256:05ce0be39ad85740a78750c86a93485c40f08ad8c62a6006de0233765996e5c7", @@ -879,10 +932,10 @@ }, "certifi": { "hashes": [ - "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", - "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" + "sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd", + "sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4" ], - "version": "==2020.6.20" + "version": "==2020.11.8" }, "chardet": { "hashes": [ From cb2340539ded2e6044e5386b79198d4539d0383a Mon Sep 17 00:00:00 2001 From: Jonas Winkler <jonas.winkler@jpwinkler.de> Date: Thu, 19 Nov 2020 22:14:11 +0100 Subject: [PATCH 0007/1708] updated pipenv --- Pipfile.lock | 86 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 69 insertions(+), 17 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 932dc380e..ebe2c4095 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "65b67f06b1ad7d6541a1f84552f1c24a0353ed03aa4c382f8638c9d112b41eda" + "sha256": "ef3638ed4905e0809823dc1cfefd8e5ee415cd9d33ec3f23e483fb60b87d6fe6" }, "pipfile-spec": 6, "requires": {}, @@ -10,6 +10,11 @@ "name": "pypi", "url": "https://pypi.python.org/simple", "verify_ssl": true + }, + { + "name": "piwheels", + "url": "https://www.piwheels.org/simple", + "verify_ssl": true } ] }, @@ -55,6 +60,7 @@ }, "autobahn": { "hashes": [ + "sha256:1eafbbe363a7924fd21bb0b94ece9f3ac2a9aa9c2046e8a85e044f94e8ba2028", "sha256:24ce276d313e84d68241c3aef30d484f352b90a40168981b3640312c821df77b", "sha256:86bbce30cdd407137c57670993a8f9bfdfe3f8e994b889181d85e844d5aa8dfb" ], @@ -64,7 +70,8 @@ "automat": { "hashes": [ "sha256:7979803c74610e11ef0c0d68a2942b152df52da55336e0c9d58daf1831cbdf33", - "sha256:b6feb6455337df834f6c9962d6ccf771515b7d939bca142b29c20c2376bc6111" + "sha256:b6feb6455337df834f6c9962d6ccf771515b7d939bca142b29c20c2376bc6111", + "sha256:d6d976cf8da698fc85fa7def46e2544493f78cb7ee72d2f4acd1a5c759a3060e" ], "version": "==20.2.0" }, @@ -82,6 +89,7 @@ "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4", "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f", "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3", + "sha256:178a2db1589cb9b0b5b28a74ee0c9d4438bd96f8c6c0ac85662ff3c98f7f8d20", "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579", "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537", "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e", @@ -108,6 +116,7 @@ "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d", "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b", "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e", + "sha256:df90c0c9e383e8c3bdced39f113ecc36fa9c623dd04dd1b5199e9edc53389a95", "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d", "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730", "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394", @@ -143,12 +152,14 @@ "hashes": [ "sha256:07ca431b788249af92764e3be9a488aa1d39a0bc3be313d826bbec690417e538", "sha256:13b88a0bd044b4eae1ef40e265d006e34dbcde0c2f1e15eb9896501b2d8f6c6f", + "sha256:257dab4f368fae15f378ea9a4d2799bf3696668062de0e9fa0ebb7a738a6917d", "sha256:32434673d8505b42c0de4de86da8c1620651abd24afe91ae0335597683ed1b77", "sha256:3cd75a683b15576cfc822c7c5742b3276e50b21a06672dc3a800a2d5da4ecd1b", "sha256:4e7268a0ca14536fecfdf2b00297d4e407da904718658c1ff1961c713f90fd33", "sha256:545a8550782dda68f8cdc75a6e3bf252017aa8f75f19f5a9ca940772fc0cb56e", "sha256:55d0b896631412b6f0c7de56e12eb3e261ac347fbaa5d5e705291a9016e5f8cb", "sha256:5849d59358547bf789ee7e0d7a9036b2d29e9a4ddf1ce5e06bb45634f995c53e", + "sha256:59f7d4cfea9ef12eb9b14b83d79b432162a0a24a91ddc15c2c9bf76a68d96f2b", "sha256:6dc59630ecce8c1f558277ceb212c751d6730bd12c80ea96b4ac65637c4f55e7", "sha256:7117319b44ed1842c617d0a452383a5a052ec6aa726dfbaffa8b94c910444297", "sha256:75e8e6684cf0034f6bf2a97095cb95f81537b12b36a8fedf06e73050bb171c2d", @@ -291,6 +302,7 @@ "sha256:969843fbdfbf56cdb71da6f0bdf50f9985b8b8aeb630102945306cf10a9c6af2", "sha256:996021ef33e0f50b97ff2d6b5f422a0fe5577de21a8873b58a779a5ddd1c3132", "sha256:9e9c9078a7ce07e6fce366bd818be89365a35d2e4b163268f0ca9ba7e13bb2f6", + "sha256:9f4e67f87e072de981570eaf7cb41444bbac7e92b05c8651dbab6eb1fb8d5a14", "sha256:a04901757cb0fb0f5602ac11dda48f5510f94372144d06c2563ba56c480b467c", "sha256:a7bf1492429f18d205f3a818da3ff1f242f60aa59006e53dee00b4ef592a3363", "sha256:aa0af2deb166a5e26e0d554b824605e660039b161e37ed4f01b8d04beec184f3", @@ -298,6 +310,7 @@ "sha256:b253fe4df2afea4dfa6b1fa8c5fef212aff8bcaaeb4207e81eed05cb5e4a7919", "sha256:b27f082f47d23cffc4cf1388b84fdc45c4ef6015f906cd7e0d988d9e35d36349", "sha256:b33aea449e7f46738811fbc6f0b3177c6777a572207412bbbf6f525ffed001ae", + "sha256:b39989b49e8aca9d224324d2650029eda410a4faf43f6afb0eb4f9acb7be6097", "sha256:b44f9421c4505c548435244d74037618f452844c5d3c67719d8a55e2613549da", "sha256:bcc371151d1512201d0214c36c0c150b1dc64f19c2b1a8c9cb1d7c7c15ebd93f", "sha256:c2851deeabd96d3f6283e9c6b26e0bfed4de2dc6fb15edf913e78b79fc5909ed", @@ -315,6 +328,7 @@ }, "hyperlink": { "hashes": [ + "sha256:402c1b5fa066ea368f3118fc5a6f8505440b4d1a4ef12a844ca39332a4a29944", "sha256:47fcc7cd339c6cb2444463ec3277bdcfe142c8b1daf2160bdd52248deec815af", "sha256:c528d405766f15a2c536230de7e160b65a08e20264d8891b3eb03307b0df3c63" ], @@ -322,12 +336,21 @@ }, "idna": { "hashes": [ + "sha256:4a57a6379512ade94fa99e2fa46d3cd0f2f553040548d0e2958c6ed90ee48226", "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, + "imap-tools": { + "hashes": [ + "sha256:070929b8ec429c0aad94588a37a2962eed656a119ab61dcf91489f20fe983f5d", + "sha256:6232cd43748741496446871e889eb137351fc7a7e7f4c7888cd8c0fa28e20cda" + ], + "index": "pypi", + "version": "==0.31.0" + }, "incremental": { "hashes": [ "sha256:717e12246dddf231a349175f48d74d93e2897244939173b01974ab6661406b9f", @@ -354,12 +377,14 @@ "msgpack": { "hashes": [ "sha256:002a0d813e1f7b60da599bdf969e632074f9eec1b96cbed8fb0973a63160a408", + "sha256:0e7b5a69ec5645b0a85baaa354c29acd89eb879aaa89e7f4b37ed4d9c5abafe0", "sha256:25b3bc3190f3d9d965b818123b7752c5dfb953f0d774b454fd206c18fe384fb8", "sha256:271b489499a43af001a2e42f42d876bb98ccaa7e20512ff37ca78c8e12e68f84", "sha256:39c54fdebf5fa4dda733369012c59e7d085ebdfe35b6cf648f09d16708f1be5d", "sha256:4233b7f86c1208190c78a525cd3828ca1623359ef48f78a6fea4b91bb995775a", "sha256:5bea44181fc8e18eed1d0cd76e355073f00ce232ff9653a0ae88cb7d9e643322", "sha256:5dba6d074fac9b24f29aaf1d2d032306c27f04187651511257e7831733293ec2", + "sha256:71604047feea609ad65f5b837ec89a4de084d55a80f8af7331745a075c3dbd23", "sha256:7a22c965588baeb07242cb561b63f309db27a07382825fc98aecaf0827c1538e", "sha256:908944e3f038bca67fcfedb7845c4a257c7749bf9818632586b53bcf06ba4b97", "sha256:9534d5cc480d4aff720233411a1f765be90885750b07df772380b34c10ecb5c0", @@ -370,7 +395,8 @@ "sha256:db685187a415f51d6b937257474ca72199f393dad89534ebbdd7d7a3b000080e", "sha256:e35b051077fc2f3ce12e7c6a34cf309680c63a842db3a0616ea6ed25ad20d272", "sha256:e7bbdd8e2b277b77782f3ce34734b0dfde6cbe94ddb74de8d733d603c7f9e2b1", - "sha256:ea41c9219c597f1d2bf6b374d951d310d58684b5de9dc4bd2976db9e1e22c140" + "sha256:ea41c9219c597f1d2bf6b374d951d310d58684b5de9dc4bd2976db9e1e22c140", + "sha256:f7c80ff32171193f18a127ea357118b920020cc0acb0730016bbda02b892a2d2" ], "version": "==1.0.0" }, @@ -389,6 +415,7 @@ "sha256:448ebb1b3bf64c0267d6b09a7cba26b5ae61b6d2dbabff7c91b660c7eccf2bdb", "sha256:50e86c076611212ca62e5a59f518edafe0c0730f7d9195fec718da1a5c2bb1fc", "sha256:5734bdc0342aba9dfc6f04920988140fb41234db42381cf7ccba64169f9fe7ac", + "sha256:5ddd1dfa2be066595c1993165b4cae84b9866b12339d0c903db7f21a094324a3", "sha256:64324f64f90a9e4ef732be0928be853eee378fd6a01be21a0a8469c4f2682c83", "sha256:6ae6c680f3ebf1cf7ad1d7748868b39d9f900836df774c453c11c5440bc15b36", "sha256:6d7593a705d662be5bfe24111af14763016765f43cb6923ed86223f965f52387", @@ -416,7 +443,8 @@ }, "pathtools": { "hashes": [ - "sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0" + "sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0", + "sha256:d77d982475e87f32b82157a43b09f0a5ef3e66c1d8f3c7eb8d2580e783cd8202" ], "version": "==0.1.2" }, @@ -711,6 +739,7 @@ "sha256:9ad4fcddcbf5dc67619379782e6aeef41218a79e17979aaed01ed099876c0e62", "sha256:a254b98dbcc744c723a838c03b74a8a34c0558c9ac5c86d5561703362231107d", "sha256:b03c4338d6d3d299e8ca494194c0ae4f611548da59e3c038813f1a43976cb437", + "sha256:b5e9d3e4474644915809d6aa1416ff20430a3ed9ae723a5d295da5ddb24985e2", "sha256:cc1f78ebc982cd0602c9a7615d878396bec94908db67d4ecddca864d049112f2", "sha256:d6d25c41a009e3c6b7e757338948d0076ee1dd1770d1c09ec131f11946883c54", "sha256:d84cadd7d7998433334c99fa55bcba0d8b4aeff0edb123b2a1dfcface538e474", @@ -759,9 +788,11 @@ "tls" ], "hashes": [ + "sha256:0150dae5adc962d15e00054cc6926f1e64763fb8dd26e1632593ac06e592104b", "sha256:040eb6641125d2a9a09cf198ec7b83dd8858c6f51f6770325ed9959c00f5098f", "sha256:147780b8caf21ba2aef3688628eaf13d7e7fe02a86747cd54bfaf2140538f042", "sha256:158ddb80719a4813d292293ac44ba41d8b56555ed009d90994a278237ee63d2c", + "sha256:15e52271f08f62e2230ff093e0278aa01c9dac057c4557cadadd2429eed86a3e", "sha256:2182000d6ffc05d269e6c03bfcec8b57e20259ca1086180edaedec3f1e689292", "sha256:25ffcf37944bdad4a99981bc74006d735a678d2b5c193781254fbbb6d69e3b22", "sha256:3281d9ce889f7b21bdb73658e887141aa45a102baf3b2320eafcfba954fcefec", @@ -836,6 +867,7 @@ "hashes": [ "sha256:05a97ba92c1c7c26f25c9f671aa1ef85ffead6cdad13770e5b689cf983adc7e1", "sha256:07d61722dd7d85547b7c6b0f5486b4338001fab349f2ac5cabc0b7182eb3425d", + "sha256:09fc3922f235703c0b76f8234867685eee68a24a49fffa2220975f6142db45f1", "sha256:0a990dcc97806e5980bbb54b2e46b9cde9e48932d8e6984daf71ef1745516123", "sha256:150e8bcb7253a34a4535aeea3de36c0bb3b1a6a47a183a95d65a194b3e07f232", "sha256:1743bcfe45af8846b775086471c28258f4c6e9ee8ef37484de4495f15a98b549", @@ -870,6 +902,7 @@ "sha256:844fad925ac5c2ad4faaceb3b2520ad016b5280105c6e16e79838cf951903a7b", "sha256:8ceb3667dd13b8133f2e4d637b5b00f240f066448e2aa89a41f4c2d78a26ce50", "sha256:92dc0fb79675882d0b6138be4bf0cec7ea7c7eede60aaca78303d8e8dbdaa523", + "sha256:974f5957e66a7524ea81df7b2686a456bfaf0408dbb7353ddfbedb594eadfef6", "sha256:9789bd945e9f5bd026ed3f5b453d640befb8b1fc33a779c1fe8d3eb21fe3fb4a", "sha256:a2b6d6eb693bc2fc6c484f2e5d93bd0b0da803fa77bf974f160533e555e4d095", "sha256:aab9f1e34d810feb00bf841993552b8fcc6ae71d473c505381627143d0018a6a", @@ -924,11 +957,11 @@ }, "babel": { "hashes": [ - "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38", - "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4" + "sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5", + "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.0" + "version": "==2.9.0" }, "certifi": { "hashes": [ @@ -954,6 +987,7 @@ "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f", "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7", "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c", + "sha256:3188a7dfd96f734a7498f37cde6598b1e9c084f1ca68bc1aa04e88db31168ab6", "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5", "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7", "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729", @@ -979,7 +1013,8 @@ "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237", "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7", "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636", - "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8" + "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8", + "sha256:ef221855191457fffeb909d5787d1807800ab4d0111f089e6c93ee68f577634d" ], "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" @@ -1001,6 +1036,7 @@ }, "docopt": { "hashes": [ + "sha256:15fde8252aa9f2804171014d50d069ffbf42c7a50b7d74bcbb82bfd5700fcfc2", "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" ], "version": "==0.6.2" @@ -1031,11 +1067,11 @@ }, "faker": { "hashes": [ - "sha256:6afc461ab3f779c9c16e299fc731d775e39ea7e8e063b3053ee359ae198a15ca", - "sha256:ce1c38823eb0f927567cde5bf2e7c8ca565c7a70316139342050ce2ca74b4026" + "sha256:3f5d379e4b5ce92a8afe3c2ce59d7c43886370dd3bf9495a936b91888debfc81", + "sha256:8c0e8a06acef4b9312902e2ce18becabe62badd3a6632180bd0680c6ee111473" ], "markers": "python_version >= '3.5'", - "version": "==4.14.2" + "version": "==4.17.0" }, "filelock": { "hashes": [ @@ -1046,6 +1082,7 @@ }, "idna": { "hashes": [ + "sha256:4a57a6379512ade94fa99e2fa46d3cd0f2f553040548d0e2958c6ed90ee48226", "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], @@ -1063,12 +1100,14 @@ "iniconfig": { "hashes": [ "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", + "sha256:8647b85c03813b8680f4ae9c9db2fd7293f8591ea536a10d73d90f6eb4b10aac", "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" ], "version": "==1.1.1" }, "jinja2": { "hashes": [ + "sha256:3f172970d5670703bd3812e8ca6459a9a7e069fa8e51b40195f83c81db191ec4", "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" ], @@ -1082,8 +1121,10 @@ "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", + "sha256:19536834abffb3fa155017053c607cb835b2ecc6a3a2554a88043d991dffb736", "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:3d61f15e39611aacd91b7e71d903787da86d9e80896e683c0103fced9add7834", "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", @@ -1093,6 +1134,7 @@ "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", + "sha256:7952deddf24b85c88dab48f6ec366ac6e39d2761b5280f2f9594911e03fcd064", "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", @@ -1195,6 +1237,7 @@ }, "pytest-forked": { "hashes": [ + "sha256:2d1bfc93ab65a28324eb0a63503bfb500c2da6916efede7a24b43a04970fe63c", "sha256:6aa9ac7e00ad1a539c41bec6d21011332de671e938c7637378ec9710204e37ca", "sha256:dc4147784048e70ef5d437951728825a131b81714b398d5d52f17c7c144d8815" ], @@ -1233,11 +1276,11 @@ }, "requests": { "hashes": [ - "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", - "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" + "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8", + "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==2.24.0" + "version": "==2.25.0" }, "six": { "hashes": [ @@ -1262,6 +1305,14 @@ "index": "pypi", "version": "==3.3.0" }, + "sphinx-rtd-theme": { + "hashes": [ + "sha256:22c795ba2832a169ca301cd0a083f7a434e09c538c70beb42782c073651b707d", + "sha256:373413d0f82425aaa28fb288009bf0d0964711d347763af2f1b65cafcb028c82" + ], + "index": "pypi", + "version": "==0.5.0" + }, "sphinxcontrib-applehelp": { "hashes": [ "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", @@ -1312,6 +1363,7 @@ }, "termcolor": { "hashes": [ + "sha256:19b1225d03bfb56571484caaa8521d8ec6e2473ae1640c9f48a48dda49417706", "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b" ], "version": "==1.1.0" @@ -1341,11 +1393,11 @@ }, "urllib3": { "hashes": [ - "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", - "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" + "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.25.11" + "version": "==1.26.2" }, "virtualenv": { "hashes": [ From 391020a2b064bff95fa9116599a36fb11129a214 Mon Sep 17 00:00:00 2001 From: Jonas Winkler <jonas.winkler@jpwinkler.de> Date: Fri, 20 Nov 2020 10:58:17 +0100 Subject: [PATCH 0008/1708] small changes --- src/documents/consumer.py | 6 +++--- src/paperless/settings.py | 18 +++++++++--------- src/paperless_tesseract/parsers.py | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/documents/consumer.py b/src/documents/consumer.py index 973fff925..8edbb00a3 100755 --- a/src/documents/consumer.py +++ b/src/documents/consumer.py @@ -223,9 +223,9 @@ class Consumer(LoggingMixin): self.log("debug", "Deleting file {}".format(self.path)) os.unlink(self.path) except Exception as e: - raise ConsumerError(e) - self._send_progress(file, 100, 100, 'FAILED', + self._send_progress(self.filename, 100, 100, 'FAILED', "Failed: {}".format(e)) + raise ConsumerError(e) finally: document_parser.cleanup() @@ -234,7 +234,7 @@ class Consumer(LoggingMixin): "Document {} consumption finished".format(document) ) - self._send_progress(file, 100, 100, 'SUCCESS', + self._send_progress(self.filename, 100, 100, 'SUCCESS', 'Finished.', document.id) return document diff --git a/src/paperless/settings.py b/src/paperless/settings.py index cb115739b..37b046b2a 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -143,6 +143,15 @@ TEMPLATES = [ }, ] +CHANNEL_LAYERS = { + "default": { + "BACKEND": "channels_redis.core.RedisChannelLayer", + "CONFIG": { + "hosts": ["redis://localhost:6379"], + }, + }, +} + ############################################################################### # Security # ############################################################################### @@ -376,12 +385,3 @@ for t in json.loads(os.getenv("PAPERLESS_FILENAME_PARSE_TRANSFORMS", "[]")): # TODO: this should not have a prefix. # Specify the filename format for out files PAPERLESS_FILENAME_FORMAT = os.getenv("PAPERLESS_FILENAME_FORMAT") - -CHANNEL_LAYERS = { - "default": { - "BACKEND": "channels_redis.core.RedisChannelLayer", - "CONFIG": { - "hosts": [("broker", 6379)], - }, - }, -} diff --git a/src/paperless_tesseract/parsers.py b/src/paperless_tesseract/parsers.py index c3715c83a..11a6c8b37 100644 --- a/src/paperless_tesseract/parsers.py +++ b/src/paperless_tesseract/parsers.py @@ -90,7 +90,7 @@ class RasterisedDocumentParser(DocumentParser): self._text = get_text_from_pdf(self.document_path) return self._text - self.progress_callback(0,1,"Making greyscale images.") + self.progress_callback(0, 1, "Making greyscale images.") images = self._get_greyscale() if not images: From 6cf0b851b7011d86afeced9deb777fdbf374cfc4 Mon Sep 17 00:00:00 2001 From: Jonas Winkler <jonas.winkler@jpwinkler.de> Date: Sun, 22 Nov 2020 23:12:24 +0100 Subject: [PATCH 0009/1708] post-merge changes --- Pipfile.lock | 430 +++++++++++++++++- src-ui/src/app/app.module.ts | 2 + .../consumer-status-widget.component.html | 18 +- ... => consumer-status-widget.component.scss} | 0 .../consumer-status-widget.component.ts | 2 +- .../file-upload-widget.component.css | 0 .../file-upload-widget.component.html | 11 - .../file-upload-widget.component.spec.ts | 25 - .../file-upload-widget.component.ts | 44 -- .../upload-file-widget.component.ts | 4 +- 10 files changed, 437 insertions(+), 99 deletions(-) rename src-ui/src/app/components/dashboard/widgets/consumer-status-widget/{consumer-status-widget.component.css => consumer-status-widget.component.scss} (100%) delete mode 100644 src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.css delete mode 100644 src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.html delete mode 100644 src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.spec.ts delete mode 100644 src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.ts diff --git a/Pipfile.lock b/Pipfile.lock index 6ecca3c34..2dd198e14 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ae2643b9cf0cf5741ae149fb6bc0c480de41329ce48e773eb4b5d760bc5e2244" + "sha256": "83cb61d0f0de0ad70aa02e8424deb743331c3578a67ee17ed06394506fdb2c14" }, "pipfile-spec": 6, "requires": {}, @@ -19,6 +19,13 @@ ] }, "default": { + "aioredis": { + "hashes": [ + "sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a", + "sha256:b61808d7e97b7cd5a92ed574937a079c9387fdadd22bfbfa7ad2fd319ecc26e3" + ], + "version": "==1.3.1" + }, "arrow": { "hashes": [ "sha256:e098abbd9af3665aea81bdd6c869e93af4feb078e98468dd351c383af187aac5", @@ -35,6 +42,39 @@ "markers": "python_version >= '3.5'", "version": "==3.3.1" }, + "async-timeout": { + "hashes": [ + "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", + "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" + ], + "markers": "python_full_version >= '3.5.3'", + "version": "==3.0.1" + }, + "attrs": { + "hashes": [ + "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", + "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.3.0" + }, + "autobahn": { + "hashes": [ + "sha256:1eafbbe363a7924fd21bb0b94ece9f3ac2a9aa9c2046e8a85e044f94e8ba2028", + "sha256:24ce276d313e84d68241c3aef30d484f352b90a40168981b3640312c821df77b", + "sha256:86bbce30cdd407137c57670993a8f9bfdfe3f8e994b889181d85e844d5aa8dfb" + ], + "markers": "python_version >= '3.5'", + "version": "==20.7.1" + }, + "automat": { + "hashes": [ + "sha256:7979803c74610e11ef0c0d68a2942b152df52da55336e0c9d58daf1831cbdf33", + "sha256:b6feb6455337df834f6c9962d6ccf771515b7d939bca142b29c20c2376bc6111", + "sha256:d6d976cf8da698fc85fa7def46e2544493f78cb7ee72d2f4acd1a5c759a3060e" + ], + "version": "==20.2.0" + }, "blessed": { "hashes": [ "sha256:7d4914079a6e8e14fbe080dcaf14dee596a088057cdc598561080e3266123b48", @@ -42,6 +82,110 @@ ], "version": "==1.17.11" }, + "cffi": { + "hashes": [ + "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d", + "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b", + "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4", + "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f", + "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3", + "sha256:178a2db1589cb9b0b5b28a74ee0c9d4438bd96f8c6c0ac85662ff3c98f7f8d20", + "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579", + "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537", + "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e", + "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05", + "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171", + "sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca", + "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522", + "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c", + "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc", + "sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d", + "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808", + "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828", + "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869", + "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d", + "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9", + "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0", + "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc", + "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15", + "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c", + "sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a", + "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3", + "sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1", + "sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768", + "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d", + "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b", + "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e", + "sha256:df90c0c9e383e8c3bdced39f113ecc36fa9c623dd04dd1b5199e9edc53389a95", + "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d", + "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730", + "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394", + "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1", + "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591" + ], + "version": "==1.14.3" + }, + "channels": { + "hashes": [ + "sha256:74db79c9eca616be69d38013b22083ab5d3f9ccda1ab5e69096b1bb7da2d9b18", + "sha256:f50a6e79757a64c1e45e95e144a2ac5f1e99ee44a0718ab182c501f5e5abd268" + ], + "index": "pypi", + "version": "==3.0.2" + }, + "channels-redis": { + "hashes": [ + "sha256:18d63f6462a58011740dc8eeb57ea4b31ec220eb551cb71b27de9c6779a549de", + "sha256:2fb31a63b05373f6402da2e6a91a22b9e66eb8b56626c6bfc93e156c734c5ae6" + ], + "index": "pypi", + "version": "==3.2.0" + }, + "constantly": { + "hashes": [ + "sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35", + "sha256:dd2fa9d6b1a51a83f0d7dd76293d734046aa176e384bf6e33b7e44880eb37c5d" + ], + "version": "==15.1.0" + }, + "cryptography": { + "hashes": [ + "sha256:07ca431b788249af92764e3be9a488aa1d39a0bc3be313d826bbec690417e538", + "sha256:13b88a0bd044b4eae1ef40e265d006e34dbcde0c2f1e15eb9896501b2d8f6c6f", + "sha256:257dab4f368fae15f378ea9a4d2799bf3696668062de0e9fa0ebb7a738a6917d", + "sha256:32434673d8505b42c0de4de86da8c1620651abd24afe91ae0335597683ed1b77", + "sha256:3cd75a683b15576cfc822c7c5742b3276e50b21a06672dc3a800a2d5da4ecd1b", + "sha256:4e7268a0ca14536fecfdf2b00297d4e407da904718658c1ff1961c713f90fd33", + "sha256:545a8550782dda68f8cdc75a6e3bf252017aa8f75f19f5a9ca940772fc0cb56e", + "sha256:55d0b896631412b6f0c7de56e12eb3e261ac347fbaa5d5e705291a9016e5f8cb", + "sha256:5849d59358547bf789ee7e0d7a9036b2d29e9a4ddf1ce5e06bb45634f995c53e", + "sha256:59f7d4cfea9ef12eb9b14b83d79b432162a0a24a91ddc15c2c9bf76a68d96f2b", + "sha256:6dc59630ecce8c1f558277ceb212c751d6730bd12c80ea96b4ac65637c4f55e7", + "sha256:7117319b44ed1842c617d0a452383a5a052ec6aa726dfbaffa8b94c910444297", + "sha256:75e8e6684cf0034f6bf2a97095cb95f81537b12b36a8fedf06e73050bb171c2d", + "sha256:7b8d9d8d3a9bd240f453342981f765346c87ade811519f98664519696f8e6ab7", + "sha256:a035a10686532b0587d58a606004aa20ad895c60c4d029afa245802347fab57b", + "sha256:a4e27ed0b2504195f855b52052eadcc9795c59909c9d84314c5408687f933fc7", + "sha256:a733671100cd26d816eed39507e585c156e4498293a907029969234e5e634bc4", + "sha256:a75f306a16d9f9afebfbedc41c8c2351d8e61e818ba6b4c40815e2b5740bb6b8", + "sha256:bd717aa029217b8ef94a7d21632a3bb5a4e7218a4513d2521c2a2fd63011e98b", + "sha256:d25cecbac20713a7c3bc544372d42d8eafa89799f492a43b79e1dfd650484851", + "sha256:d26a2557d8f9122f9bf445fc7034242f4375bd4e95ecda007667540270965b13", + "sha256:d3545829ab42a66b84a9aaabf216a4dce7f16dbc76eb69be5c302ed6b8f4a29b", + "sha256:d3d5e10be0cf2a12214ddee45c6bd203dab435e3d83b4560c03066eda600bfe3", + "sha256:efe15aca4f64f3a7ea0c09c87826490e50ed166ce67368a68f315ea0807a20df" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==3.2.1" + }, + "daphne": { + "hashes": [ + "sha256:0052c9887600c57054a5867d4b0240159fa009faa3bcf6a1627271d9cdcb005a", + "sha256:c22b692707f514de9013651ecb687f2abe4f35cf6fe292ece634e9f1737bc7e3" + ], + "index": "pypi", + "version": "==3.0.1" + }, "dateparser": { "hashes": [ "sha256:7552c994f893b5cb8fcf103b4cd2ff7f57aab9bfd2619fdf0cf571c0740fd90b", @@ -121,6 +265,77 @@ "index": "pypi", "version": "==20.0.4" }, + "hiredis": { + "hashes": [ + "sha256:06a039208f83744a702279b894c8cf24c14fd63c59cd917dcde168b79eef0680", + "sha256:0a909bf501459062aa1552be1461456518f367379fdc9fdb1f2ca5e4a1fdd7c0", + "sha256:18402d9e54fb278cb9a8c638df6f1550aca36a009d47ecf5aa263a38600f35b0", + "sha256:1e4cbbc3858ec7e680006e5ca590d89a5e083235988f26a004acf7244389ac01", + "sha256:23344e3c2177baf6975fbfa361ed92eb7d36d08f454636e5054b3faa7c2aff8a", + "sha256:289b31885b4996ce04cadfd5fc03d034dce8e2a8234479f7c9e23b9e245db06b", + "sha256:2c1c570ae7bf1bab304f29427e2475fe1856814312c4a1cf1cd0ee133f07a3c6", + "sha256:2c227c0ed371771ffda256034427320870e8ea2e4fd0c0a618c766e7c49aad73", + "sha256:3bb9b63d319402cead8bbd9dd55dca3b667d2997e9a0d8a1f9b6cc274db4baee", + "sha256:3ef2183de67b59930d2db8b8e8d4d58e00a50fcc5e92f4f678f6eed7a1c72d55", + "sha256:43b8ed3dbfd9171e44c554cb4acf4ee4505caa84c5e341858b50ea27dd2b6e12", + "sha256:47bcf3c5e6c1e87ceb86cdda2ee983fa0fe56a999e6185099b3c93a223f2fa9b", + "sha256:5263db1e2e1e8ae30500cdd75a979ff99dcc184201e6b4b820d0de74834d2323", + "sha256:5b1451727f02e7acbdf6aae4e06d75f66ee82966ff9114550381c3271a90f56c", + "sha256:6996883a8a6ff9117cbb3d6f5b0dcbbae6fb9e31e1a3e4e2f95e0214d9a1c655", + "sha256:6c96f64a54f030366657a54bb90b3093afc9c16c8e0dfa29fc0d6dbe169103a5", + "sha256:7332d5c3e35154cd234fd79573736ddcf7a0ade7a986db35b6196b9171493e75", + "sha256:7885b6f32c4a898e825bb7f56f36a02781ac4a951c63e4169f0afcf9c8c30dfb", + "sha256:7b0f63f10a166583ab744a58baad04e0f52cfea1ac27bfa1b0c21a48d1003c23", + "sha256:819f95d4eba3f9e484dd115ab7ab72845cf766b84286a00d4ecf76d33f1edca1", + "sha256:8968eeaa4d37a38f8ca1f9dbe53526b69628edc9c42229a5b2f56d98bb828c1f", + "sha256:89ebf69cb19a33d625db72d2ac589d26e936b8f7628531269accf4a3196e7872", + "sha256:8daecd778c1da45b8bd54fd41ffcd471a86beed3d8e57a43acf7a8d63bba4058", + "sha256:955ba8ea73cf3ed8bd2f963b4cb9f8f0dcb27becd2f4b3dd536fd24c45533454", + "sha256:964f18a59f5a64c0170f684c417f4fe3e695a536612e13074c4dd5d1c6d7c882", + "sha256:969843fbdfbf56cdb71da6f0bdf50f9985b8b8aeb630102945306cf10a9c6af2", + "sha256:996021ef33e0f50b97ff2d6b5f422a0fe5577de21a8873b58a779a5ddd1c3132", + "sha256:9e9c9078a7ce07e6fce366bd818be89365a35d2e4b163268f0ca9ba7e13bb2f6", + "sha256:9f4e67f87e072de981570eaf7cb41444bbac7e92b05c8651dbab6eb1fb8d5a14", + "sha256:a04901757cb0fb0f5602ac11dda48f5510f94372144d06c2563ba56c480b467c", + "sha256:a7bf1492429f18d205f3a818da3ff1f242f60aa59006e53dee00b4ef592a3363", + "sha256:aa0af2deb166a5e26e0d554b824605e660039b161e37ed4f01b8d04beec184f3", + "sha256:abfb15a6a7822f0fae681785cb38860e7a2cb1616a708d53df557b3d76c5bfd4", + "sha256:b253fe4df2afea4dfa6b1fa8c5fef212aff8bcaaeb4207e81eed05cb5e4a7919", + "sha256:b27f082f47d23cffc4cf1388b84fdc45c4ef6015f906cd7e0d988d9e35d36349", + "sha256:b33aea449e7f46738811fbc6f0b3177c6777a572207412bbbf6f525ffed001ae", + "sha256:b39989b49e8aca9d224324d2650029eda410a4faf43f6afb0eb4f9acb7be6097", + "sha256:b44f9421c4505c548435244d74037618f452844c5d3c67719d8a55e2613549da", + "sha256:bcc371151d1512201d0214c36c0c150b1dc64f19c2b1a8c9cb1d7c7c15ebd93f", + "sha256:c2851deeabd96d3f6283e9c6b26e0bfed4de2dc6fb15edf913e78b79fc5909ed", + "sha256:cdfd501c7ac5b198c15df800a3a34c38345f5182e5f80770caf362bccca65628", + "sha256:d2c0caffa47606d6d7c8af94ba42547bd2a441f06c74fd90a1ffe328524a6c64", + "sha256:dcb2db95e629962db5a355047fb8aefb012df6c8ae608930d391619dbd96fd86", + "sha256:e0eeb9c112fec2031927a1745788a181d0eecbacbed941fc5c4f7bc3f7b273bf", + "sha256:e154891263306200260d7f3051982774d7b9ef35af3509d5adbbe539afd2610c", + "sha256:e2e023a42dcbab8ed31f97c2bcdb980b7fbe0ada34037d87ba9d799664b58ded", + "sha256:e64be68255234bb489a574c4f2f8df7029c98c81ec4d160d6cd836e7f0679390", + "sha256:e82d6b930e02e80e5109b678c663a9ed210680ded81c1abaf54635d88d1da298" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.1.0" + }, + "hyperlink": { + "hashes": [ + "sha256:402c1b5fa066ea368f3118fc5a6f8505440b4d1a4ef12a844ca39332a4a29944", + "sha256:47fcc7cd339c6cb2444463ec3277bdcfe142c8b1daf2160bdd52248deec815af", + "sha256:c528d405766f15a2c536230de7e160b65a08e20264d8891b3eb03307b0df3c63" + ], + "version": "==20.0.1" + }, + "idna": { + "hashes": [ + "sha256:4a57a6379512ade94fa99e2fa46d3cd0f2f553040548d0e2958c6ed90ee48226", + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" + }, "imap-tools": { "hashes": [ "sha256:96e9a4ff6483462635737730a1df28e739faa71967b12a84f4363fb386542246", @@ -129,6 +344,13 @@ "index": "pypi", "version": "==0.32.0" }, + "incremental": { + "hashes": [ + "sha256:717e12246dddf231a349175f48d74d93e2897244939173b01974ab6661406b9f", + "sha256:7b751696aaf36eebfab537e458929e194460051ccad279c72b755a167eebd4b3" + ], + "version": "==17.5.0" + }, "joblib": { "hashes": [ "sha256:698c311779f347cf6b7e6b8a39bb682277b8ee4aba8cf9507bc0cf4cd4737b72", @@ -146,6 +368,32 @@ "index": "pypi", "version": "==1.0.8" }, + "msgpack": { + "hashes": [ + "sha256:002a0d813e1f7b60da599bdf969e632074f9eec1b96cbed8fb0973a63160a408", + "sha256:0e7b5a69ec5645b0a85baaa354c29acd89eb879aaa89e7f4b37ed4d9c5abafe0", + "sha256:25b3bc3190f3d9d965b818123b7752c5dfb953f0d774b454fd206c18fe384fb8", + "sha256:271b489499a43af001a2e42f42d876bb98ccaa7e20512ff37ca78c8e12e68f84", + "sha256:39c54fdebf5fa4dda733369012c59e7d085ebdfe35b6cf648f09d16708f1be5d", + "sha256:4233b7f86c1208190c78a525cd3828ca1623359ef48f78a6fea4b91bb995775a", + "sha256:5bea44181fc8e18eed1d0cd76e355073f00ce232ff9653a0ae88cb7d9e643322", + "sha256:5dba6d074fac9b24f29aaf1d2d032306c27f04187651511257e7831733293ec2", + "sha256:71604047feea609ad65f5b837ec89a4de084d55a80f8af7331745a075c3dbd23", + "sha256:7a22c965588baeb07242cb561b63f309db27a07382825fc98aecaf0827c1538e", + "sha256:908944e3f038bca67fcfedb7845c4a257c7749bf9818632586b53bcf06ba4b97", + "sha256:9534d5cc480d4aff720233411a1f765be90885750b07df772380b34c10ecb5c0", + "sha256:aa5c057eab4f40ec47ea6f5a9825846be2ff6bf34102c560bad5cad5a677c5be", + "sha256:b3758dfd3423e358bbb18a7cccd1c74228dffa7a697e5be6cb9535de625c0dbf", + "sha256:c901e8058dd6653307906c5f157f26ed09eb94a850dddd989621098d347926ab", + "sha256:cec8bf10981ed70998d98431cd814db0ecf3384e6b113366e7f36af71a0fca08", + "sha256:db685187a415f51d6b937257474ca72199f393dad89534ebbdd7d7a3b000080e", + "sha256:e35b051077fc2f3ce12e7c6a34cf309680c63a842db3a0616ea6ed25ad20d272", + "sha256:e7bbdd8e2b277b77782f3ce34734b0dfde6cbe94ddb74de8d733d603c7f9e2b1", + "sha256:ea41c9219c597f1d2bf6b374d951d310d58684b5de9dc4bd2976db9e1e22c140", + "sha256:f7c80ff32171193f18a127ea357118b920020cc0acb0730016bbda02b892a2d2" + ], + "version": "==1.0.0" + }, "numpy": { "hashes": [ "sha256:08308c38e44cc926bdfce99498b21eec1f848d24c302519e64203a8da99a97db", @@ -287,6 +535,58 @@ "index": "pypi", "version": "==2.8.6" }, + "pyasn1": { + "hashes": [ + "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", + "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576", + "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", + "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", + "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", + "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", + "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", + "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86", + "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12", + "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776", + "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", + "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2", + "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3" + ], + "version": "==0.4.8" + }, + "pyasn1-modules": { + "hashes": [ + "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8", + "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199", + "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811", + "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed", + "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4", + "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", + "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74", + "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb", + "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45", + "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd", + "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0", + "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d", + "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405" + ], + "version": "==0.2.8" + }, + "pycparser": { + "hashes": [ + "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", + "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.20" + }, + "pyhamcrest": { + "hashes": [ + "sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316", + "sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29" + ], + "markers": "python_version >= '3.5'", + "version": "==2.0.2" + }, "pyocr": { "hashes": [ "sha256:fa15adc7e1cf0d345a2990495fe125a947c6e09a60ddba0256a1c14b2e603179", @@ -295,6 +595,13 @@ "index": "pypi", "version": "==0.7.2" }, + "pyopenssl": { + "hashes": [ + "sha256:621880965a720b8ece2f1b2f54ea2071966ab00e2970ad2ce11d596102063504", + "sha256:9a24494b2602aaf402be5c9e30a0b82d4a5c67528fe8fb475e3f3bc00dd69507" + ], + "version": "==19.1.0" + }, "python-dateutil": { "hashes": [ "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", @@ -456,6 +763,13 @@ "markers": "python_version >= '3.6'", "version": "==1.5.4" }, + "service-identity": { + "hashes": [ + "sha256:001c0707759cb3de7e49c078a7c0c9cd12594161d3bf06b9c254fdcb1a60dc36", + "sha256:0858a54aabc5b459d1aafa8a518ed2081a285087f349fe3e55197989232e2e2d" + ], + "version": "==18.1.0" + }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", @@ -480,6 +794,48 @@ "markers": "python_version >= '3.5'", "version": "==2.1.0" }, + "twisted": { + "extras": [ + "tls" + ], + "hashes": [ + "sha256:0150dae5adc962d15e00054cc6926f1e64763fb8dd26e1632593ac06e592104b", + "sha256:040eb6641125d2a9a09cf198ec7b83dd8858c6f51f6770325ed9959c00f5098f", + "sha256:147780b8caf21ba2aef3688628eaf13d7e7fe02a86747cd54bfaf2140538f042", + "sha256:158ddb80719a4813d292293ac44ba41d8b56555ed009d90994a278237ee63d2c", + "sha256:15e52271f08f62e2230ff093e0278aa01c9dac057c4557cadadd2429eed86a3e", + "sha256:2182000d6ffc05d269e6c03bfcec8b57e20259ca1086180edaedec3f1e689292", + "sha256:25ffcf37944bdad4a99981bc74006d735a678d2b5c193781254fbbb6d69e3b22", + "sha256:3281d9ce889f7b21bdb73658e887141aa45a102baf3b2320eafcfba954fcefec", + "sha256:356e8d8dd3590e790e3dba4db139eb8a17aca64b46629c622e1b1597a4a92478", + "sha256:70952c56e4965b9f53b180daecf20a9595cf22b8d0935cd3bd664c90273c3ab2", + "sha256:7408c6635ee1b96587289283ebe90ee15dbf9614b05857b446055116bc822d29", + "sha256:7c547fd0215db9da8a1bc23182b309e84a232364cc26d829e9ee196ce840b114", + "sha256:894f6f3cfa57a15ea0d0714e4283913a5f2511dbd18653dd148eba53b3919797", + "sha256:94ac3d55a58c90e2075c5fe1853f2aa3892b73e3bf56395f743aefde8605eeaa", + "sha256:a58e61a2a01e5bcbe3b575c0099a2bcb8d70a75b1a087338e0c48dd6e01a5f15", + "sha256:c09c47ff9750a8e3aa60ad169c4b95006d455a29b80ad0901f031a103b2991cd", + "sha256:ca3a0b8c9110800e576d89b5337373e52018b41069bc879f12fa42b7eb2d0274", + "sha256:cd1dc5c85b58494138a3917752b54bb1daa0045d234b7c132c37a61d5483ebad", + "sha256:cdbc4c7f0cd7a2218b575844e970f05a1be1861c607b0e048c9bceca0c4d42f7", + "sha256:d267125cc0f1e8a0eed6319ba4ac7477da9b78a535601c49ecd20c875576433a", + "sha256:d72c55b5d56e176563b91d11952d13b01af8725c623e498db5507b6614fc1e10", + "sha256:d95803193561a243cb0401b0567c6b7987d3f2a67046770e1dccd1c9e49a9780", + "sha256:e92703bed0cc21d6cb5c61d66922b3b1564015ca8a51325bd164a5e33798d504", + "sha256:f058bd0168271de4dcdc39845b52dd0a4a2fecf5f1246335f13f5e96eaebb467", + "sha256:f3c19e5bd42bbe4bf345704ad7c326c74d3fd7a1b3844987853bef180be638d4" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==20.3.0" + }, + "txaio": { + "hashes": [ + "sha256:17938f2bca4a9cabce61346758e482ca4e600160cbc28e861493eac74a19539d", + "sha256:38a469daf93c37e5527cb062653d6393ae11663147c42fab7ddc3f6d00d434ae" + ], + "markers": "python_version >= '3.5'", + "version": "==20.4.1" + }, "tzlocal": { "hashes": [ "sha256:643c97c5294aedc737780a49d9df30889321cbe1204eac2c2ec6134035a92e44", @@ -518,6 +874,66 @@ ], "index": "pypi", "version": "==2.7.4" + }, + "zope.interface": { + "hashes": [ + "sha256:05a97ba92c1c7c26f25c9f671aa1ef85ffead6cdad13770e5b689cf983adc7e1", + "sha256:07d61722dd7d85547b7c6b0f5486b4338001fab349f2ac5cabc0b7182eb3425d", + "sha256:09fc3922f235703c0b76f8234867685eee68a24a49fffa2220975f6142db45f1", + "sha256:0a990dcc97806e5980bbb54b2e46b9cde9e48932d8e6984daf71ef1745516123", + "sha256:150e8bcb7253a34a4535aeea3de36c0bb3b1a6a47a183a95d65a194b3e07f232", + "sha256:1743bcfe45af8846b775086471c28258f4c6e9ee8ef37484de4495f15a98b549", + "sha256:1b5f6c8fff4ed32aa2dd43e84061bc8346f32d3ba6ad6e58f088fe109608f102", + "sha256:21e49123f375703cf824214939d39df0af62c47d122d955b2a8d9153ea08cfd5", + "sha256:21f579134a47083ffb5ddd1307f0405c91aa8b61ad4be6fd5af0171474fe0c45", + "sha256:27c267dc38a0f0079e96a2945ee65786d38ef111e413c702fbaaacbab6361d00", + "sha256:299bde0ab9e5c4a92f01a152b7fbabb460f31343f1416f9b7b983167ab1e33bc", + "sha256:2ab88d8f228f803fcb8cb7d222c579d13dab2d3622c51e8cf321280da01102a7", + "sha256:2ced4c35061eea623bc84c7711eedce8ecc3c2c51cd9c6afa6290df3bae9e104", + "sha256:2dcab01c660983ba5e5a612e0c935141ccbee67d2e2e14b833e01c2354bd8034", + "sha256:32546af61a9a9b141ca38d971aa6eb9800450fa6620ce6323cc30eec447861f3", + "sha256:32b40a4c46d199827d79c86bb8cb88b1bbb764f127876f2cb6f3a47f63dbada3", + "sha256:3cc94c69f6bd48ed86e8e24f358cb75095c8129827df1298518ab860115269a4", + "sha256:42b278ac0989d6f5cf58d7e0828ea6b5951464e3cf2ff229dd09a96cb6ba0c86", + "sha256:495b63fd0302f282ee6c1e6ea0f1c12cb3d1a49c8292d27287f01845ff252a96", + "sha256:4af87cdc0d4b14e600e6d3d09793dce3b7171348a094ba818e2a68ae7ee67546", + "sha256:4b94df9f2fdde7b9314321bab8448e6ad5a23b80542dcab53e329527d4099dcb", + "sha256:4c48ddb63e2b20fba4c6a2bf81b4d49e99b6d4587fb67a6cd33a2c1f003af3e3", + "sha256:4df9afd17bd5477e9f8c8b6bb8507e18dd0f8b4efe73bb99729ff203279e9e3b", + "sha256:518950fe6a5d56f94ba125107895f938a4f34f704c658986eae8255edb41163b", + "sha256:538298e4e113ccb8b41658d5a4b605bebe75e46a30ceca22a5a289cf02c80bec", + "sha256:55465121e72e208a7b69b53de791402affe6165083b2ea71b892728bd19ba9ae", + "sha256:588384d70a0f19b47409cfdb10e0c27c20e4293b74fc891df3d8eb47782b8b3e", + "sha256:6278c080d4afffc9016e14325f8734456831124e8c12caa754fd544435c08386", + "sha256:64ea6c221aeee4796860405e1aedec63424cda4202a7ad27a5066876db5b0fd2", + "sha256:681dbb33e2b40262b33fd383bae63c36d33fd79fa1a8e4092945430744ffd34a", + "sha256:6936aa9da390402d646a32a6a38d5409c2d2afb2950f045a7d02ab25a4e7d08d", + "sha256:778d0ec38bbd288b150a3ae363c8ffd88d2207a756842495e9bffd8a8afbc89a", + "sha256:8251f06a77985a2729a8bdbefbae79ee78567dddc3acbd499b87e705ca59fe24", + "sha256:83b4aa5344cce005a9cff5d0321b2e318e871cc1dfc793b66c32dd4f59e9770d", + "sha256:844fad925ac5c2ad4faaceb3b2520ad016b5280105c6e16e79838cf951903a7b", + "sha256:8ceb3667dd13b8133f2e4d637b5b00f240f066448e2aa89a41f4c2d78a26ce50", + "sha256:92dc0fb79675882d0b6138be4bf0cec7ea7c7eede60aaca78303d8e8dbdaa523", + "sha256:974f5957e66a7524ea81df7b2686a456bfaf0408dbb7353ddfbedb594eadfef6", + "sha256:9789bd945e9f5bd026ed3f5b453d640befb8b1fc33a779c1fe8d3eb21fe3fb4a", + "sha256:a2b6d6eb693bc2fc6c484f2e5d93bd0b0da803fa77bf974f160533e555e4d095", + "sha256:aab9f1e34d810feb00bf841993552b8fcc6ae71d473c505381627143d0018a6a", + "sha256:abb61afd84f23099ac6099d804cdba9bd3b902aaaded3ffff47e490b0a495520", + "sha256:adf9ee115ae8ff8b6da4b854b4152f253b390ba64407a22d75456fe07dcbda65", + "sha256:aedc6c672b351afe6dfe17ff83ee5e7eb6ed44718f879a9328a68bdb20b57e11", + "sha256:b7a00ecb1434f8183395fac5366a21ee73d14900082ca37cf74993cf46baa56c", + "sha256:ba32f4a91c1cb7314c429b03afbf87b1fff4fb1c8db32260e7310104bd77f0c7", + "sha256:cbd0f2cbd8689861209cd89141371d3a22a11613304d1f0736492590aa0ab332", + "sha256:e4bc372b953bf6cec65a8d48482ba574f6e051621d157cf224227dbb55486b1e", + "sha256:eccac3d9aadc68e994b6d228cb0c8919fc47a5350d85a1b4d3d81d1e98baf40c", + "sha256:efd550b3da28195746bb43bd1d815058181a7ca6d9d6aa89dd37f5eefe2cacb7", + "sha256:efef581c8ba4d990770875e1a2218e856849d32ada2680e53aebc5d154a17e20", + "sha256:f057897711a630a0b7a6a03f1acf379b6ba25d37dc5dc217a97191984ba7f2fc", + "sha256:f37d45fab14ffef9d33a0dc3bc59ce0c5313e2253323312d47739192da94f5fd", + "sha256:f44906f70205d456d503105023041f1e63aece7623b31c390a0103db4de17537" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==5.2.0" } }, "develop": { @@ -663,11 +1079,11 @@ }, "faker": { "hashes": [ - "sha256:3f5d379e4b5ce92a8afe3c2ce59d7c43886370dd3bf9495a936b91888debfc81", - "sha256:8c0e8a06acef4b9312902e2ce18becabe62badd3a6632180bd0680c6ee111473" + "sha256:5398268e1d751ffdb3ed36b8a790ed98659200599b368eec38a02eed15bce997", + "sha256:d4183b8f57316de3be27cd6c3b40e9f9343d27c95c96179f027316c58c2c239e" ], "markers": "python_version >= '3.5'", - "version": "==4.17.0" + "version": "==4.17.1" }, "filelock": { "hashes": [ @@ -999,11 +1415,11 @@ }, "virtualenv": { "hashes": [ - "sha256:b0011228208944ce71052987437d3843e05690b2f23d1c7da4263fde104c97a2", - "sha256:b8d6110f493af256a40d65e29846c69340a947669eec8ce784fcf3dd3af28380" + "sha256:6af42359fbb33a6c7eab4d3246524b96fd9d8e07e7141b7a65998f96e28b2c57", + "sha256:fd4147c5ba3f694e2e4fc3c767407dc2226899623bb9b49c2f15637c2ee335b3" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.1.0" + "version": "==20.2.0" } } } diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index 3ccb1c5f1..2a70af813 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -45,6 +45,7 @@ import { SavedViewWidgetComponent } from './components/dashboard/widgets/saved-v import { StatisticsWidgetComponent } from './components/dashboard/widgets/statistics-widget/statistics-widget.component'; import { UploadFileWidgetComponent } from './components/dashboard/widgets/upload-file-widget/upload-file-widget.component'; import { WidgetFrameComponent } from './components/dashboard/widgets/widget-frame/widget-frame.component'; +import { ConsumerStatusWidgetComponent } from './components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component'; @NgModule({ declarations: [ @@ -82,6 +83,7 @@ import { WidgetFrameComponent } from './components/dashboard/widgets/widget-fram SavedViewWidgetComponent, StatisticsWidgetComponent, UploadFileWidgetComponent, + ConsumerStatusWidgetComponent, WidgetFrameComponent ], imports: [ diff --git a/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.html b/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.html index d6559e184..ff2117729 100644 --- a/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.html +++ b/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.html @@ -1,10 +1,10 @@ -<h4 class="mt-3">Document consumer status</h4> - -<div class="mb-2 border-bottom" *ngFor="let s of getStatus()"> - <div class="mb-1"><strong>{{s.filename}}:</strong> {{s.message}}</div> - <ngb-progressbar [type]="getType(s.status)" [value]="s.current_progress" [max]="s.max_progress" class="mb-2"></ngb-progressbar> - <div *ngIf="isFinished(s)" class="mb-2"> - <button *ngIf="s.document_id" class="btn btn-sm btn-outline-primary mr-2" routerLink="/documents/{{s.document_id}}" (click)="dismiss(s)">Open document</button> - <button class="btn btn-sm btn-outline-secondary" (click)="dismiss(s)">Dismiss</button> +<app-widget-frame title="Document consumer status"> + <div class="mb-2 border-bottom" *ngFor="let s of getStatus()"> + <div class="mb-1"><strong>{{s.filename}}:</strong> {{s.message}}</div> + <ngb-progressbar [type]="getType(s.status)" [value]="s.current_progress" [max]="s.max_progress" class="mb-2"></ngb-progressbar> + <div *ngIf="isFinished(s)" class="mb-2"> + <button *ngIf="s.document_id" class="btn btn-sm btn-outline-primary mr-2" routerLink="/documents/{{s.document_id}}" (click)="dismiss(s)">Open document</button> + <button class="btn btn-sm btn-outline-secondary" (click)="dismiss(s)">Dismiss</button> + </div> </div> -</div> \ No newline at end of file +</app-widget-frame> diff --git a/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.css b/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.scss similarity index 100% rename from src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.css rename to src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.scss diff --git a/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.ts b/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.ts index 0c4e35682..8e44af6d5 100644 --- a/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.ts +++ b/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.ts @@ -4,7 +4,7 @@ import { ConsumerStatusService, FileStatus } from 'src/app/services/consumer-sta @Component({ selector: 'app-consumer-status-widget', templateUrl: './consumer-status-widget.component.html', - styleUrls: ['./consumer-status-widget.component.css'] + styleUrls: ['./consumer-status-widget.component.scss'] }) export class ConsumerStatusWidgetComponent implements OnInit { diff --git a/src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.css b/src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.html b/src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.html deleted file mode 100644 index 0c5ea634a..000000000 --- a/src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.html +++ /dev/null @@ -1,11 +0,0 @@ -<h4>Upload new Document</h4> -<form> - <ngx-file-drop - dropZoneLabel="Drop documents here" - (onFileDrop)="dropped($event)" - (onFileOver)="fileOver($event)" - (onFileLeave)="fileLeave($event)" - dropZoneClassName="bg-light mt-4 card"> - - </ngx-file-drop> -</form> \ No newline at end of file diff --git a/src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.spec.ts b/src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.spec.ts deleted file mode 100644 index 847f5288b..000000000 --- a/src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { FileUploadWidgetComponent } from './file-upload-widget.component'; - -describe('FileUploadWidgetComponent', () => { - let component: FileUploadWidgetComponent; - let fixture: ComponentFixture<FileUploadWidgetComponent>; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ FileUploadWidgetComponent ] - }) - .compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(FileUploadWidgetComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.ts b/src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.ts deleted file mode 100644 index 5d4bac936..000000000 --- a/src-ui/src/app/components/dashboard/widgets/file-upload-widget/file-upload-widget.component.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop'; -import { DocumentService } from 'src/app/services/rest/document.service'; -import { ToastService } from 'src/app/services/toast.service'; - -@Component({ - selector: 'app-file-upload-widget', - templateUrl: './file-upload-widget.component.html', - styleUrls: ['./file-upload-widget.component.css'] -}) -export class FileUploadWidgetComponent implements OnInit { - - constructor(private documentService: DocumentService, private toastService: ToastService) { } - - ngOnInit(): void { - } - - public fileOver(event){ - console.log(event); - } - - public fileLeave(event){ - console.log(event); - } - - public dropped(files: NgxFileDropEntry[]) { - for (const droppedFile of files) { - if (droppedFile.fileEntry.isFile) { - const fileEntry = droppedFile.fileEntry as FileSystemFileEntry; - console.log(fileEntry) - fileEntry.file((file: File) => { - console.log(file) - const formData = new FormData() - formData.append('document', file, file.name) - this.documentService.uploadDocument(formData).subscribe(result => { - this.toastService.showInfo("The document has been uploaded and will be processed by the consumer shortly.") - }, error => { - this.toastService.showError("An error has occured while uploading the document. Sorry!") - }) - }); - } - } - } -} diff --git a/src-ui/src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.ts b/src-ui/src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.ts index a95d5f4db..cb13b2d74 100644 --- a/src-ui/src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.ts +++ b/src-ui/src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.ts @@ -33,9 +33,9 @@ export class UploadFileWidgetComponent implements OnInit { const formData = new FormData() formData.append('document', file, file.name) this.documentService.uploadDocument(formData).subscribe(result => { - this.toastService.showToast(Toast.make("Information", "The document has been uploaded and will be processed by the consumer shortly.")) + this.toastService.showInfo("The document has been uploaded and will be processed by the consumer shortly.") }, error => { - this.toastService.showToast(Toast.makeError("An error has occured while uploading the document. Sorry!")) + this.toastService.showError("An error has occured while uploading the document. Sorry!") }) }); } From 32186e0de1328e8213edb265c3e1f98b06a6c019 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Sun, 29 Nov 2020 16:33:33 +0100 Subject: [PATCH 0010/1708] added a menu for bulk edits. --- .../document-list.component.html | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index cc682b8e3..d142fbb04 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -1,5 +1,31 @@ <app-page-header [title]="getTitle()"> + <div ngbDropdown class="d-inline-block mr-2"> + <button class="btn btn-sm btn-outline-primary" id="dropdownBasic1" ngbDropdownToggle> + <svg class="toolbaricon" fill="currentColor"> + <use xlink:href="assets/bootstrap-icons.svg#text-indent-left" /> + </svg> + Bulk edit + </button> + <div ngbDropdownMenu aria-labelledby="dropdownBasic1"> + <button ngbDropdownItem>Select page</button> + <button ngbDropdownItem>Select all</button> + <button ngbDropdownItem>Select none</button> + <div class="dropdown-divider"></div> + <button ngbDropdownItem>Re-create archived document</button> + <div class="dropdown-divider"></div> + <button ngbDropdownItem>Set correspondent</button> + <button ngbDropdownItem>Remove correspondent</button> + <button ngbDropdownItem>Set document type</button> + <button ngbDropdownItem>Remove document type</button> + <button ngbDropdownItem>Add tag</button> + <button ngbDropdownItem>Remove tag</button> + <div class="dropdown-divider"></div> + <button ngbDropdownItem>Delete</button> + + </div> + </div> + <div class="btn-group btn-group-toggle" ngbRadioGroup [(ngModel)]="displayMode" (ngModelChange)="saveDisplayMode()"> <label ngbButtonLabel class="btn-outline-primary btn-sm"> From fd4c9a1758b550218ba7fd62f762cc7313c69923 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Sun, 29 Nov 2020 23:00:52 +0100 Subject: [PATCH 0011/1708] not sure if this works --- src/documents/forms.py | 12 ++++++++++++ src/documents/views.py | 11 +++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/documents/forms.py b/src/documents/forms.py index 63dd307b2..0c73c3810 100644 --- a/src/documents/forms.py +++ b/src/documents/forms.py @@ -1,4 +1,5 @@ import os +import re import tempfile from datetime import datetime from time import mktime @@ -11,6 +12,17 @@ from pathvalidate import validate_filename, ValidationError from documents.parsers import is_mime_type_supported +class BuldEditForm(forms.Form): + + def clean_ids(self): + ids = self.cleaned_data.get("ids") + if not re.match(r"[0-9,]+", ids): + raise forms.ValidationError("id list invalid") + id_list = [int(id) for id in ids.split(",")] + + + + class UploadForm(forms.Form): diff --git a/src/documents/views.py b/src/documents/views.py index 84f4a3999..ee45af267 100755 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -93,6 +93,10 @@ class DocumentTypeViewSet(ModelViewSet): ordering_fields = ("name", "matching_algorithm", "match", "document_count") +class BulkEditForm(object): + pass + + class DocumentViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, @@ -149,6 +153,13 @@ class DocumentViewSet(RetrieveModelMixin, else: return HttpResponseBadRequest(str(form.errors)) + @action(methods=['post'], detail=False) + def bulk_edit(self, request, pk=None): + form = BulkEditForm(data=request.POST) + if not form.is_valid(): + return HttpResponseBadRequest("") + return Response({'asd': request.POST['content']}) + @action(methods=['get'], detail=True) def metadata(self, request, pk=None): try: From 35124023f0a207a6b127210b8661688325aa541e Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Mon, 30 Nov 2020 13:58:40 +0100 Subject: [PATCH 0012/1708] basic support for bulk editing. --- .../src/app/services/rest/document.service.ts | 8 ++++ src/documents/bulk_edit.py | 39 +++++++++++++++++++ src/documents/forms.py | 12 ------ src/documents/views.py | 9 +++-- 4 files changed, 52 insertions(+), 16 deletions(-) create mode 100644 src/documents/bulk_edit.py diff --git a/src-ui/src/app/services/rest/document.service.ts b/src-ui/src/app/services/rest/document.service.ts index cdea89914..07e69c87a 100644 --- a/src-ui/src/app/services/rest/document.service.ts +++ b/src-ui/src/app/services/rest/document.service.ts @@ -66,4 +66,12 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument> return this.http.post(this.getResourceUrl(null, 'post_document'), formData) } + bulk_edit(ids: number[], method: string, args: any[]) { + return this.http.post(this.getResourceUrl(null, 'bulk_edit'), { + 'ids': ids, + 'method': method, + 'args': args + }) + } + } diff --git a/src/documents/bulk_edit.py b/src/documents/bulk_edit.py new file mode 100644 index 000000000..ef5d3f509 --- /dev/null +++ b/src/documents/bulk_edit.py @@ -0,0 +1,39 @@ +from documents.models import Document + + +methods_supported = [ + "set_correspondent" +] + + +def validate_data(data): + if 'ids' not in data or not isinstance(data['ids'], list): + raise ValueError() + ids = data['ids'] + if not all([isinstance(i, int) for i in ids]): + raise ValueError() + count = Document.objects.filter(pk__in=ids).count() + if not count == len(ids): + raise Document.DoesNotExist() + + if 'method' not in data or not isinstance(data['method'], str): + raise ValueError() + method = data['method'] + if method not in methods_supported: + raise ValueError() + + if 'args' not in data or not isinstance(data['args'], list): + raise ValueError() + parameters = data['args'] + + return ids, method, parameters + + +def perform_bulk_edit(data): + ids, method, args = validate_data(data) + + getattr(__file__, method)(ids, args) + + +def set_correspondent(ids, args): + print("WOW") diff --git a/src/documents/forms.py b/src/documents/forms.py index 0c73c3810..63dd307b2 100644 --- a/src/documents/forms.py +++ b/src/documents/forms.py @@ -1,5 +1,4 @@ import os -import re import tempfile from datetime import datetime from time import mktime @@ -12,17 +11,6 @@ from pathvalidate import validate_filename, ValidationError from documents.parsers import is_mime_type_supported -class BuldEditForm(forms.Form): - - def clean_ids(self): - ids = self.cleaned_data.get("ids") - if not re.match(r"[0-9,]+", ids): - raise forms.ValidationError("id list invalid") - id_list = [int(id) for id in ids.split(",")] - - - - class UploadForm(forms.Form): diff --git a/src/documents/views.py b/src/documents/views.py index ee45af267..95448ad62 100755 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -23,6 +23,7 @@ from rest_framework.viewsets import ( import documents.index as index from paperless.db import GnuPG from paperless.views import StandardPagination +from .bulk_edit import perform_bulk_edit from .filters import ( CorrespondentFilterSet, DocumentFilterSet, @@ -155,10 +156,10 @@ class DocumentViewSet(RetrieveModelMixin, @action(methods=['post'], detail=False) def bulk_edit(self, request, pk=None): - form = BulkEditForm(data=request.POST) - if not form.is_valid(): - return HttpResponseBadRequest("") - return Response({'asd': request.POST['content']}) + try: + return Response(perform_bulk_edit(request.data)) + except Exception as e: + return HttpResponseBadRequest(str(e)) @action(methods=['get'], detail=True) def metadata(self, request, pk=None): From 26784a532551b5e52bc3868a102a5e594a6110eb Mon Sep 17 00:00:00 2001 From: jayme-github <jayme-github@users.noreply.github.com> Date: Thu, 3 Dec 2020 20:12:55 +0100 Subject: [PATCH 0013/1708] Add automatic coloring of tags Please see this as proposal on how to implement automatic/random colors for tags while keeping the current set of hard coded colors in place (at least in the frontend). Bear with me as I have even less Angular knowledge than Django and just tried to get away with as few changes as possible. :-) AIUI you want to change to a color picking system anyways in the future, which could also play well with this. fixes #51 --- Pipfile | 1 + .../components/common/tag/tag.component.html | 4 +- .../components/common/tag/tag.component.ts | 6 +- .../tag-edit-dialog.component.html | 2 +- .../tag-edit-dialog.component.ts | 6 +- .../manage/tag-list/tag-list.component.html | 2 +- .../manage/tag-list/tag-list.component.ts | 8 ++- src-ui/src/app/data/paperless-tag.ts | 31 ++++---- .../migrations/1006_migrate_tag_colour.py | 70 +++++++++++++++++++ src/documents/models.py | 28 +++----- 10 files changed, 116 insertions(+), 42 deletions(-) create mode 100644 src/documents/migrations/1006_migrate_tag_colour.py diff --git a/Pipfile b/Pipfile index c0728fddf..1cf5a312f 100644 --- a/Pipfile +++ b/Pipfile @@ -40,6 +40,7 @@ whoosh="~=2.7.4" inotifyrecursive = ">=0.3.4" ocrmypdf = "*" tqdm = "*" +colorhash = "*" [dev-packages] coveralls = "*" diff --git a/src-ui/src/app/components/common/tag/tag.component.html b/src-ui/src/app/components/common/tag/tag.component.html index 8b9632a65..29c554142 100644 --- a/src-ui/src/app/components/common/tag/tag.component.html +++ b/src-ui/src/app/components/common/tag/tag.component.html @@ -1,2 +1,2 @@ -<span *ngIf="!clickable" class="badge" [style.background]="getColour().value" [style.color]="getColour().textColor">{{tag.name}}</span> -<a [routerLink]="" [title]="linkTitle" *ngIf="clickable" class="badge" [style.background]="getColour().value" [style.color]="getColour().textColor">{{tag.name}}</a> \ No newline at end of file +<span *ngIf="!clickable" class="badge" [style.background]="getColour().id" [style.color]="getColour().textColor">{{tag.name}}</span> +<a [routerLink]="" [title]="linkTitle" *ngIf="clickable" class="badge" [style.background]="getColour().id" [style.color]="getColour().textColor">{{tag.name}}</a> \ No newline at end of file diff --git a/src-ui/src/app/components/common/tag/tag.component.ts b/src-ui/src/app/components/common/tag/tag.component.ts index c032c51db..163960f3d 100644 --- a/src-ui/src/app/components/common/tag/tag.component.ts +++ b/src-ui/src/app/components/common/tag/tag.component.ts @@ -23,7 +23,11 @@ export class TagComponent implements OnInit { } getColour() { - return TAG_COLOURS.find(c => c.id == this.tag.colour) + var color = TAG_COLOURS.find(c => c.id == this.tag.colour) + if (color) { + return color + } + return { id: this.tag.colour, name: this.tag.colour, textColor: "#ffffff" } } } diff --git a/src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.html b/src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.html index 8048b0c80..2f6dded52 100644 --- a/src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.html +++ b/src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.html @@ -7,7 +7,7 @@ </div> <div class="modal-body"> <app-input-text title="Name" formControlName="name"></app-input-text> - <app-input-select title="Colour" [items]="getColours()" formControlName="colour" [textColor]="getColor(objectForm.value.colour).textColor" [backgroundColor]="getColor(objectForm.value.colour).value"></app-input-select> + <app-input-select title="Colour" [items]="getColours()" formControlName="colour" [textColor]="getColor(objectForm.value.colour).textColor" [backgroundColor]="getColor(objectForm.value.colour).id"></app-input-select> <app-input-check title="Inbox tag" formControlName="is_inbox_tag" hint="Inbox tags are automatically assigned to all consumed documents."></app-input-check> <app-input-text title="Match" formControlName="match"></app-input-text> <app-input-select title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select> diff --git a/src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.ts b/src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.ts index bb0162608..8017f275b 100644 --- a/src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.ts +++ b/src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.ts @@ -13,14 +13,14 @@ import { ToastService } from 'src/app/services/toast.service'; }) export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> { - constructor(service: TagService, activeModal: NgbActiveModal, toastService: ToastService) { + constructor(service: TagService, activeModal: NgbActiveModal, toastService: ToastService) { super(service, activeModal, toastService, 'tag') } getForm(): FormGroup { return new FormGroup({ name: new FormControl(''), - colour: new FormControl(1), + colour: new FormControl(''), is_inbox_tag: new FormControl(false), matching_algorithm: new FormControl(1), match: new FormControl(""), @@ -32,7 +32,7 @@ export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> { return TAG_COLOURS } - getColor(id: number) { + getColor(id) { return TAG_COLOURS.find(c => c.id == id) } diff --git a/src-ui/src/app/components/manage/tag-list/tag-list.component.html b/src-ui/src/app/components/manage/tag-list/tag-list.component.html index e68b997d1..8923ad210 100644 --- a/src-ui/src/app/components/manage/tag-list/tag-list.component.html +++ b/src-ui/src/app/components/manage/tag-list/tag-list.component.html @@ -23,7 +23,7 @@ <tr *ngFor="let tag of data"> <td scope="row">{{ tag.name }}</td> <td scope="row"><span class="badge" [style.color]="getColor(tag.colour).textColor" - [style.background-color]="getColor(tag.colour).value">{{ getColor(tag.colour).name }}</span></td> + [style.background-color]="tag.colour">{{ getColor(tag.colour).name }}</span></td> <td scope="row">{{ getMatching(tag) }}</td> <td scope="row">{{ tag.document_count }}</td> <td scope="row"> diff --git a/src-ui/src/app/components/manage/tag-list/tag-list.component.ts b/src-ui/src/app/components/manage/tag-list/tag-list.component.ts index 761a9484c..582835de1 100644 --- a/src-ui/src/app/components/manage/tag-list/tag-list.component.ts +++ b/src-ui/src/app/components/manage/tag-list/tag-list.component.ts @@ -15,10 +15,14 @@ export class TagListComponent extends GenericListComponent<PaperlessTag> { constructor(tagService: TagService, modalService: NgbModal) { super(tagService, modalService, TagEditDialogComponent) - } + } getColor(id) { - return TAG_COLOURS.find(c => c.id == id) + var color = TAG_COLOURS.find(c => c.id == id) + if (color) { + return color + } + return { id: id, name: id, textColor: "#ffffff" } } getObjectName(object: PaperlessTag) { diff --git a/src-ui/src/app/data/paperless-tag.ts b/src-ui/src/app/data/paperless-tag.ts index 551c6e03a..3d8ddfdaf 100644 --- a/src-ui/src/app/data/paperless-tag.ts +++ b/src-ui/src/app/data/paperless-tag.ts @@ -3,26 +3,27 @@ import { ObjectWithId } from './object-with-id'; export const TAG_COLOURS = [ - {id: 1, value: "#a6cee3", name: "Light Blue", textColor: "#000000"}, - {id: 2, value: "#1f78b4", name: "Blue", textColor: "#ffffff"}, - {id: 3, value: "#b2df8a", name: "Light Green", textColor: "#000000"}, - {id: 4, value: "#33a02c", name: "Green", textColor: "#000000"}, - {id: 5, value: "#fb9a99", name: "Light Red", textColor: "#000000"}, - {id: 6, value: "#e31a1c", name: "Red ", textColor: "#ffffff"}, - {id: 7, value: "#fdbf6f", name: "Light Orange", textColor: "#000000"}, - {id: 8, value: "#ff7f00", name: "Orange", textColor: "#000000"}, - {id: 9, value: "#cab2d6", name: "Light Violet", textColor: "#000000"}, - {id: 10, value: "#6a3d9a", name: "Violet", textColor: "#ffffff"}, - {id: 11, value: "#b15928", name: "Brown", textColor: "#000000"}, - {id: 12, value: "#000000", name: "Black", textColor: "#ffffff"}, - {id: 13, value: "#cccccc", name: "Light Grey", textColor: "#000000"} + { id: "", name: "Auto", textColor: "#000000" }, + { id: "#a6cee3", name: "Light Blue", textColor: "#000000" }, + { id: "#1f78b4", name: "Blue", textColor: "#ffffff" }, + { id: "#b2df8a", name: "Light Green", textColor: "#000000" }, + { id: "#33a02c", name: "Green", textColor: "#000000" }, + { id: "#fb9a99", name: "Light Red", textColor: "#000000" }, + { id: "#e31a1c", name: "Red ", textColor: "#ffffff" }, + { id: "#fdbf6f", name: "Light Orange", textColor: "#000000" }, + { id: "#ff7f00", name: "Orange", textColor: "#000000" }, + { id: "#cab2d6", name: "Light Violet", textColor: "#000000" }, + { id: "#6a3d9a", name: "Violet", textColor: "#ffffff" }, + { id: "#b15928", name: "Brown", textColor: "#000000" }, + { id: "#000000", name: "Black", textColor: "#ffffff" }, + { id: "#cccccc", name: "Light Grey", textColor: "#000000" } ] export interface PaperlessTag extends MatchingModel { - colour?: number + colour?: string is_inbox_tag?: boolean - + document_count?: number } diff --git a/src/documents/migrations/1006_migrate_tag_colour.py b/src/documents/migrations/1006_migrate_tag_colour.py new file mode 100644 index 000000000..d9bea8355 --- /dev/null +++ b/src/documents/migrations/1006_migrate_tag_colour.py @@ -0,0 +1,70 @@ +# Generated by Django 3.1.4 on 2020-12-02 21:43 + +from django.db import migrations, models + +COLOURS_OLD = { + 1: "#a6cee3", + 2: "#1f78b4", + 3: "#b2df8a", + 4: "#33a02c", + 5: "#fb9a99", + 6: "#e31a1c", + 7: "#fdbf6f", + 8: "#ff7f00", + 9: "#cab2d6", + 10: "#6a3d9a", + 11: "#b15928", + 12: "#000000", + 13: "#cccccc", +} + + +def forward(apps, schema_editor): + Tag = apps.get_model('documents', 'Tag') + + for tag in Tag.objects.all(): + colour_old_id = tag.colour_old + rgb = COLOURS_OLD[colour_old_id] + tag.colour = rgb + tag.save() + + +def reverse(apps, schema_editor): + Tag = apps.get_model('documents', 'Tag') + + def _get_colour_id(rdb): + for idx, rdbx in COLOURS_OLD.items(): + if rdbx == rdb: + return idx + # Return colour 1 if we can't match anything + return 1 + + for tag in Tag.objects.all(): + colour_id = _get_colour_id(tag.colour) + tag.colour_old = colour_id + tag.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('documents', '1005_checksums'), + ] + + operations = [ + migrations.RenameField( + model_name='tag', + old_name='colour', + new_name='colour_old', + ), + migrations.AddField( + model_name='tag', + name='colour', + field=models.CharField(blank=True, max_length=7), + ), + migrations.RunPython(forward, reverse), + migrations.RemoveField( + model_name='tag', + name='colour_old', + ) + ] diff --git a/src/documents/models.py b/src/documents/models.py index a4f887d77..5491e4038 100755 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -6,6 +6,7 @@ import re from collections import OrderedDict import dateutil.parser +from colorhash import ColorHash from django.conf import settings from django.db import models from django.utils import timezone @@ -84,23 +85,7 @@ class Correspondent(MatchingModel): class Tag(MatchingModel): - COLOURS = ( - (1, "#a6cee3"), - (2, "#1f78b4"), - (3, "#b2df8a"), - (4, "#33a02c"), - (5, "#fb9a99"), - (6, "#e31a1c"), - (7, "#fdbf6f"), - (8, "#ff7f00"), - (9, "#cab2d6"), - (10, "#6a3d9a"), - (11, "#b15928"), - (12, "#000000"), - (13, "#cccccc") - ) - - colour = models.PositiveIntegerField(choices=COLOURS, default=1) + colour = models.CharField(blank=True, max_length=7) is_inbox_tag = models.BooleanField( default=False, @@ -108,6 +93,15 @@ class Tag(MatchingModel): "documents will be tagged with inbox tags." ) + def save(self, *args, **kwargs): + if self.colour == "": + self.colour = ColorHash( + self.name, + lightness=(0.35, 0.45, 0.55, 0.65), + saturation=(0.2, 0.3, 0.4, 0.5)).hex + + super().save(*args, **kwargs) + class DocumentType(MatchingModel): From 5369e0be037b0ea373d655b6f7ecd246efec792d Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Sun, 6 Dec 2020 14:39:53 +0100 Subject: [PATCH 0014/1708] more bulk edit --- src/documents/bulk_edit.py | 16 +++++++++++++--- src/documents/serialisers.py | 28 ++++++++++++++++++++++++++++ src/documents/views.py | 32 ++++++++++++++++++++++++-------- 3 files changed, 65 insertions(+), 11 deletions(-) diff --git a/src/documents/bulk_edit.py b/src/documents/bulk_edit.py index ef5d3f509..f80c55c58 100644 --- a/src/documents/bulk_edit.py +++ b/src/documents/bulk_edit.py @@ -1,5 +1,4 @@ -from documents.models import Document - +from documents.models import Document, Correspondent methods_supported = [ "set_correspondent" @@ -36,4 +35,15 @@ def perform_bulk_edit(data): def set_correspondent(ids, args): - print("WOW") + if not len(args) == 1: + raise ValueError() + + if not args[0]: + correspondent = None + else: + if not isinstance(args[0], int): + raise ValueError() + + correspondent = Correspondent.objects.get(args[0]) + + Document.objects.filter(id__in=ids).update(correspondent=correspondent) diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index c988b2137..a8da79cdd 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -113,6 +113,34 @@ class LogSerializer(serializers.ModelSerializer): ) +class BulkEditSerializer(serializers.Serializer): + + documents = serializers.PrimaryKeyRelatedField( + many=True, + label="Documents", + write_only=True, + queryset=Document.objects.all() + ) + + method = serializers.ChoiceField( + choices=[ + "set_correspondent", + "set_document_type", + "add_tag", + "remove_tag", + "delete" + ], + label="Method", + write_only=True, + ) + + parameters = serializers.DictField(allow_empty=True) + + def validate(self, attrs): + + return attrs + + class PostDocumentSerializer(serializers.Serializer): document = serializers.FileField( diff --git a/src/documents/views.py b/src/documents/views.py index 219cc61b7..88ceb2efd 100755 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -46,7 +46,8 @@ from .serialisers import ( LogSerializer, TagSerializer, DocumentTypeSerializer, - PostDocumentSerializer + PostDocumentSerializer, + BulkEditSerializer ) @@ -165,13 +166,6 @@ class DocumentViewSet(RetrieveModelMixin, disposition, filename) return response - @action(methods=['post'], detail=False) - def bulk_edit(self, request, pk=None): - try: - return Response(perform_bulk_edit(request.data)) - except Exception as e: - return HttpResponseBadRequest(str(e)) - @action(methods=['get'], detail=True) def metadata(self, request, pk=None): try: @@ -225,6 +219,28 @@ class LogViewSet(ReadOnlyModelViewSet): ordering_fields = ("created",) +class BulkEditView(APIView): + + permission_classes = (IsAuthenticated,) + serializer_class = BulkEditSerializer + parser_classes = (parsers.JSONParser,) + + def get_serializer_context(self): + return { + 'request': self.request, + 'format': self.format_kwarg, + 'view': self + } + + def get_serializer(self, *args, **kwargs): + kwargs['context'] = self.get_serializer_context() + return self.serializer_class(*args, **kwargs) + + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + class PostDocumentView(APIView): permission_classes = (IsAuthenticated,) From fcaaf7ce035d56647097ff8f10ab4c84b03ac8d9 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Sun, 6 Dec 2020 22:54:11 +0100 Subject: [PATCH 0015/1708] pipfile fix --- Pipfile.lock | 337 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 335 insertions(+), 2 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 6158a70e0..13d1d74ea 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "b10db53eb22d917723aa6107ff0970dc4e2aa886ee03d3ae08a994a856d57986" + "sha256": "3faa161608e685d788b8921f80b810b176fd2b4ed9020d3e6322dffecbcb5542" }, "pipfile-spec": 6, "requires": { @@ -21,6 +21,13 @@ ] }, "default": { + "aioredis": { + "hashes": [ + "sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a", + "sha256:b61808d7e97b7cd5a92ed574937a079c9387fdadd22bfbfa7ad2fd319ecc26e3" + ], + "version": "==1.3.1" + }, "arrow": { "hashes": [ "sha256:e098abbd9af3665aea81bdd6c869e93af4feb078e98468dd351c383af187aac5", @@ -37,6 +44,39 @@ "markers": "python_version >= '3.5'", "version": "==3.3.1" }, + "async-timeout": { + "hashes": [ + "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", + "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" + ], + "markers": "python_full_version >= '3.5.3'", + "version": "==3.0.1" + }, + "attrs": { + "hashes": [ + "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", + "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.3.0" + }, + "autobahn": { + "hashes": [ + "sha256:1eafbbe363a7924fd21bb0b94ece9f3ac2a9aa9c2046e8a85e044f94e8ba2028", + "sha256:24ce276d313e84d68241c3aef30d484f352b90a40168981b3640312c821df77b", + "sha256:86bbce30cdd407137c57670993a8f9bfdfe3f8e994b889181d85e844d5aa8dfb" + ], + "markers": "python_version >= '3.5'", + "version": "==20.7.1" + }, + "automat": { + "hashes": [ + "sha256:7979803c74610e11ef0c0d68a2942b152df52da55336e0c9d58daf1831cbdf33", + "sha256:b6feb6455337df834f6c9962d6ccf771515b7d939bca142b29c20c2376bc6111", + "sha256:d6d976cf8da698fc85fa7def46e2544493f78cb7ee72d2f4acd1a5c759a3060e" + ], + "version": "==20.2.0" + }, "blessed": { "hashes": [ "sha256:0a74a8d3f0366db600d061273df77d44f0db07daade7bb7a4d49c8bc22ed9f74", @@ -87,6 +127,22 @@ ], "version": "==1.14.4" }, + "channels": { + "hashes": [ + "sha256:74db79c9eca616be69d38013b22083ab5d3f9ccda1ab5e69096b1bb7da2d9b18", + "sha256:f50a6e79757a64c1e45e95e144a2ac5f1e99ee44a0718ab182c501f5e5abd268" + ], + "index": "pypi", + "version": "==3.0.2" + }, + "channels-redis": { + "hashes": [ + "sha256:18d63f6462a58011740dc8eeb57ea4b31ec220eb551cb71b27de9c6779a549de", + "sha256:2fb31a63b05373f6402da2e6a91a22b9e66eb8b56626c6bfc93e156c734c5ae6" + ], + "index": "pypi", + "version": "==3.2.0" + }, "chardet": { "hashes": [ "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", @@ -104,6 +160,13 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==14.0" }, + "constantly": { + "hashes": [ + "sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35", + "sha256:dd2fa9d6b1a51a83f0d7dd76293d734046aa176e384bf6e33b7e44880eb37c5d" + ], + "version": "==15.1.0" + }, "cryptography": { "hashes": [ "sha256:07ca431b788249af92764e3be9a488aa1d39a0bc3be313d826bbec690417e538", @@ -134,6 +197,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==3.2.1" }, + "daphne": { + "hashes": [ + "sha256:0052c9887600c57054a5867d4b0240159fa009faa3bcf6a1627271d9cdcb005a", + "sha256:c22b692707f514de9013651ecb687f2abe4f35cf6fe292ece634e9f1737bc7e3" + ], + "index": "pypi", + "version": "==3.0.1" + }, "dateparser": { "hashes": [ "sha256:7552c994f893b5cb8fcf103b4cd2ff7f57aab9bfd2619fdf0cf571c0740fd90b", @@ -213,6 +284,60 @@ "index": "pypi", "version": "==20.0.4" }, + "hiredis": { + "hashes": [ + "sha256:06a039208f83744a702279b894c8cf24c14fd63c59cd917dcde168b79eef0680", + "sha256:0a909bf501459062aa1552be1461456518f367379fdc9fdb1f2ca5e4a1fdd7c0", + "sha256:18402d9e54fb278cb9a8c638df6f1550aca36a009d47ecf5aa263a38600f35b0", + "sha256:1e4cbbc3858ec7e680006e5ca590d89a5e083235988f26a004acf7244389ac01", + "sha256:23344e3c2177baf6975fbfa361ed92eb7d36d08f454636e5054b3faa7c2aff8a", + "sha256:289b31885b4996ce04cadfd5fc03d034dce8e2a8234479f7c9e23b9e245db06b", + "sha256:2c1c570ae7bf1bab304f29427e2475fe1856814312c4a1cf1cd0ee133f07a3c6", + "sha256:2c227c0ed371771ffda256034427320870e8ea2e4fd0c0a618c766e7c49aad73", + "sha256:3bb9b63d319402cead8bbd9dd55dca3b667d2997e9a0d8a1f9b6cc274db4baee", + "sha256:3ef2183de67b59930d2db8b8e8d4d58e00a50fcc5e92f4f678f6eed7a1c72d55", + "sha256:43b8ed3dbfd9171e44c554cb4acf4ee4505caa84c5e341858b50ea27dd2b6e12", + "sha256:47bcf3c5e6c1e87ceb86cdda2ee983fa0fe56a999e6185099b3c93a223f2fa9b", + "sha256:5263db1e2e1e8ae30500cdd75a979ff99dcc184201e6b4b820d0de74834d2323", + "sha256:5b1451727f02e7acbdf6aae4e06d75f66ee82966ff9114550381c3271a90f56c", + "sha256:6996883a8a6ff9117cbb3d6f5b0dcbbae6fb9e31e1a3e4e2f95e0214d9a1c655", + "sha256:6c96f64a54f030366657a54bb90b3093afc9c16c8e0dfa29fc0d6dbe169103a5", + "sha256:7332d5c3e35154cd234fd79573736ddcf7a0ade7a986db35b6196b9171493e75", + "sha256:7885b6f32c4a898e825bb7f56f36a02781ac4a951c63e4169f0afcf9c8c30dfb", + "sha256:7b0f63f10a166583ab744a58baad04e0f52cfea1ac27bfa1b0c21a48d1003c23", + "sha256:819f95d4eba3f9e484dd115ab7ab72845cf766b84286a00d4ecf76d33f1edca1", + "sha256:8968eeaa4d37a38f8ca1f9dbe53526b69628edc9c42229a5b2f56d98bb828c1f", + "sha256:89ebf69cb19a33d625db72d2ac589d26e936b8f7628531269accf4a3196e7872", + "sha256:8daecd778c1da45b8bd54fd41ffcd471a86beed3d8e57a43acf7a8d63bba4058", + "sha256:955ba8ea73cf3ed8bd2f963b4cb9f8f0dcb27becd2f4b3dd536fd24c45533454", + "sha256:964f18a59f5a64c0170f684c417f4fe3e695a536612e13074c4dd5d1c6d7c882", + "sha256:969843fbdfbf56cdb71da6f0bdf50f9985b8b8aeb630102945306cf10a9c6af2", + "sha256:996021ef33e0f50b97ff2d6b5f422a0fe5577de21a8873b58a779a5ddd1c3132", + "sha256:9e9c9078a7ce07e6fce366bd818be89365a35d2e4b163268f0ca9ba7e13bb2f6", + "sha256:9f4e67f87e072de981570eaf7cb41444bbac7e92b05c8651dbab6eb1fb8d5a14", + "sha256:a04901757cb0fb0f5602ac11dda48f5510f94372144d06c2563ba56c480b467c", + "sha256:a7bf1492429f18d205f3a818da3ff1f242f60aa59006e53dee00b4ef592a3363", + "sha256:aa0af2deb166a5e26e0d554b824605e660039b161e37ed4f01b8d04beec184f3", + "sha256:abfb15a6a7822f0fae681785cb38860e7a2cb1616a708d53df557b3d76c5bfd4", + "sha256:b253fe4df2afea4dfa6b1fa8c5fef212aff8bcaaeb4207e81eed05cb5e4a7919", + "sha256:b27f082f47d23cffc4cf1388b84fdc45c4ef6015f906cd7e0d988d9e35d36349", + "sha256:b33aea449e7f46738811fbc6f0b3177c6777a572207412bbbf6f525ffed001ae", + "sha256:b39989b49e8aca9d224324d2650029eda410a4faf43f6afb0eb4f9acb7be6097", + "sha256:b44f9421c4505c548435244d74037618f452844c5d3c67719d8a55e2613549da", + "sha256:bcc371151d1512201d0214c36c0c150b1dc64f19c2b1a8c9cb1d7c7c15ebd93f", + "sha256:c2851deeabd96d3f6283e9c6b26e0bfed4de2dc6fb15edf913e78b79fc5909ed", + "sha256:cdfd501c7ac5b198c15df800a3a34c38345f5182e5f80770caf362bccca65628", + "sha256:d2c0caffa47606d6d7c8af94ba42547bd2a441f06c74fd90a1ffe328524a6c64", + "sha256:dcb2db95e629962db5a355047fb8aefb012df6c8ae608930d391619dbd96fd86", + "sha256:e0eeb9c112fec2031927a1745788a181d0eecbacbed941fc5c4f7bc3f7b273bf", + "sha256:e154891263306200260d7f3051982774d7b9ef35af3509d5adbbe539afd2610c", + "sha256:e2e023a42dcbab8ed31f97c2bcdb980b7fbe0ada34037d87ba9d799664b58ded", + "sha256:e64be68255234bb489a574c4f2f8df7029c98c81ec4d160d6cd836e7f0679390", + "sha256:e82d6b930e02e80e5109b678c663a9ed210680ded81c1abaf54635d88d1da298" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.1.0" + }, "humanfriendly": { "hashes": [ "sha256:175ffa628aa76da2c17369a5da5856084562cc66dfe7f82ae93ca3ef175277a6", @@ -221,6 +346,22 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==9.0" }, + "hyperlink": { + "hashes": [ + "sha256:402c1b5fa066ea368f3118fc5a6f8505440b4d1a4ef12a844ca39332a4a29944", + "sha256:47fcc7cd339c6cb2444463ec3277bdcfe142c8b1daf2160bdd52248deec815af", + "sha256:c528d405766f15a2c536230de7e160b65a08e20264d8891b3eb03307b0df3c63" + ], + "version": "==20.0.1" + }, + "idna": { + "hashes": [ + "sha256:4a57a6379512ade94fa99e2fa46d3cd0f2f553040548d0e2958c6ed90ee48226", + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + ], + "version": "==2.10" + }, "imap-tools": { "hashes": [ "sha256:72bf46dc135b039a5d5b59f4e079242ac15eac02a30038e8cb2dec7b153cab65", @@ -244,6 +385,13 @@ "markers": "python_version < '3.8'", "version": "==3.1.1" }, + "incremental": { + "hashes": [ + "sha256:717e12246dddf231a349175f48d74d93e2897244939173b01974ab6661406b9f", + "sha256:7b751696aaf36eebfab537e458929e194460051ccad279c72b755a167eebd4b3" + ], + "version": "==17.5.0" + }, "inotify-simple": { "hashes": [ "sha256:8440ffe49c4ae81a8df57c1ae1eb4b6bfa7acb830099bfb3e305b383005cc128", @@ -322,6 +470,32 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==4.6.2" }, + "msgpack": { + "hashes": [ + "sha256:002a0d813e1f7b60da599bdf969e632074f9eec1b96cbed8fb0973a63160a408", + "sha256:0e7b5a69ec5645b0a85baaa354c29acd89eb879aaa89e7f4b37ed4d9c5abafe0", + "sha256:25b3bc3190f3d9d965b818123b7752c5dfb953f0d774b454fd206c18fe384fb8", + "sha256:271b489499a43af001a2e42f42d876bb98ccaa7e20512ff37ca78c8e12e68f84", + "sha256:39c54fdebf5fa4dda733369012c59e7d085ebdfe35b6cf648f09d16708f1be5d", + "sha256:4233b7f86c1208190c78a525cd3828ca1623359ef48f78a6fea4b91bb995775a", + "sha256:5bea44181fc8e18eed1d0cd76e355073f00ce232ff9653a0ae88cb7d9e643322", + "sha256:5dba6d074fac9b24f29aaf1d2d032306c27f04187651511257e7831733293ec2", + "sha256:71604047feea609ad65f5b837ec89a4de084d55a80f8af7331745a075c3dbd23", + "sha256:7a22c965588baeb07242cb561b63f309db27a07382825fc98aecaf0827c1538e", + "sha256:908944e3f038bca67fcfedb7845c4a257c7749bf9818632586b53bcf06ba4b97", + "sha256:9534d5cc480d4aff720233411a1f765be90885750b07df772380b34c10ecb5c0", + "sha256:aa5c057eab4f40ec47ea6f5a9825846be2ff6bf34102c560bad5cad5a677c5be", + "sha256:b3758dfd3423e358bbb18a7cccd1c74228dffa7a697e5be6cb9535de625c0dbf", + "sha256:c901e8058dd6653307906c5f157f26ed09eb94a850dddd989621098d347926ab", + "sha256:cec8bf10981ed70998d98431cd814db0ecf3384e6b113366e7f36af71a0fca08", + "sha256:db685187a415f51d6b937257474ca72199f393dad89534ebbdd7d7a3b000080e", + "sha256:e35b051077fc2f3ce12e7c6a34cf309680c63a842db3a0616ea6ed25ad20d272", + "sha256:e7bbdd8e2b277b77782f3ce34734b0dfde6cbe94ddb74de8d733d603c7f9e2b1", + "sha256:ea41c9219c597f1d2bf6b374d951d310d58684b5de9dc4bd2976db9e1e22c140", + "sha256:f7c80ff32171193f18a127ea357118b920020cc0acb0730016bbda02b892a2d2" + ], + "version": "==1.0.0" + }, "numpy": { "hashes": [ "sha256:08308c38e44cc926bdfce99498b21eec1f848d24c302519e64203a8da99a97db", @@ -514,6 +688,42 @@ "index": "pypi", "version": "==2.8.6" }, + "pyasn1": { + "hashes": [ + "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", + "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576", + "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", + "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", + "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", + "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", + "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", + "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86", + "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12", + "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776", + "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", + "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2", + "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3" + ], + "version": "==0.4.8" + }, + "pyasn1-modules": { + "hashes": [ + "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8", + "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199", + "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811", + "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed", + "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4", + "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", + "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74", + "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb", + "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45", + "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd", + "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0", + "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d", + "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405" + ], + "version": "==0.2.8" + }, "pycparser": { "hashes": [ "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", @@ -522,6 +732,21 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.20" }, + "pyhamcrest": { + "hashes": [ + "sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316", + "sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29" + ], + "markers": "python_version >= '3.5'", + "version": "==2.0.2" + }, + "pyopenssl": { + "hashes": [ + "sha256:898aefbde331ba718570244c3b01dcddb1b31a3b336613436a45e52e27d9a82d", + "sha256:92f08eccbd73701cf744e8ffd6989aa7842d48cbe3fea8a7c031c5647f590ac5" + ], + "version": "==20.0.0" + }, "python-dateutil": { "hashes": [ "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", @@ -730,6 +955,13 @@ "markers": "python_version >= '3.6'", "version": "==1.5.4" }, + "service-identity": { + "hashes": [ + "sha256:001c0707759cb3de7e49c078a7c0c9cd12594161d3bf06b9c254fdcb1a60dc36", + "sha256:0858a54aabc5b459d1aafa8a518ed2081a285087f349fe3e55197989232e2e2d" + ], + "version": "==18.1.0" + }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", @@ -769,6 +1001,48 @@ "index": "pypi", "version": "==4.54.1" }, + "twisted": { + "extras": [ + "tls" + ], + "hashes": [ + "sha256:0150dae5adc962d15e00054cc6926f1e64763fb8dd26e1632593ac06e592104b", + "sha256:040eb6641125d2a9a09cf198ec7b83dd8858c6f51f6770325ed9959c00f5098f", + "sha256:147780b8caf21ba2aef3688628eaf13d7e7fe02a86747cd54bfaf2140538f042", + "sha256:158ddb80719a4813d292293ac44ba41d8b56555ed009d90994a278237ee63d2c", + "sha256:15e52271f08f62e2230ff093e0278aa01c9dac057c4557cadadd2429eed86a3e", + "sha256:2182000d6ffc05d269e6c03bfcec8b57e20259ca1086180edaedec3f1e689292", + "sha256:25ffcf37944bdad4a99981bc74006d735a678d2b5c193781254fbbb6d69e3b22", + "sha256:3281d9ce889f7b21bdb73658e887141aa45a102baf3b2320eafcfba954fcefec", + "sha256:356e8d8dd3590e790e3dba4db139eb8a17aca64b46629c622e1b1597a4a92478", + "sha256:70952c56e4965b9f53b180daecf20a9595cf22b8d0935cd3bd664c90273c3ab2", + "sha256:7408c6635ee1b96587289283ebe90ee15dbf9614b05857b446055116bc822d29", + "sha256:7c547fd0215db9da8a1bc23182b309e84a232364cc26d829e9ee196ce840b114", + "sha256:894f6f3cfa57a15ea0d0714e4283913a5f2511dbd18653dd148eba53b3919797", + "sha256:94ac3d55a58c90e2075c5fe1853f2aa3892b73e3bf56395f743aefde8605eeaa", + "sha256:a58e61a2a01e5bcbe3b575c0099a2bcb8d70a75b1a087338e0c48dd6e01a5f15", + "sha256:c09c47ff9750a8e3aa60ad169c4b95006d455a29b80ad0901f031a103b2991cd", + "sha256:ca3a0b8c9110800e576d89b5337373e52018b41069bc879f12fa42b7eb2d0274", + "sha256:cd1dc5c85b58494138a3917752b54bb1daa0045d234b7c132c37a61d5483ebad", + "sha256:cdbc4c7f0cd7a2218b575844e970f05a1be1861c607b0e048c9bceca0c4d42f7", + "sha256:d267125cc0f1e8a0eed6319ba4ac7477da9b78a535601c49ecd20c875576433a", + "sha256:d72c55b5d56e176563b91d11952d13b01af8725c623e498db5507b6614fc1e10", + "sha256:d95803193561a243cb0401b0567c6b7987d3f2a67046770e1dccd1c9e49a9780", + "sha256:e92703bed0cc21d6cb5c61d66922b3b1564015ca8a51325bd164a5e33798d504", + "sha256:f058bd0168271de4dcdc39845b52dd0a4a2fecf5f1246335f13f5e96eaebb467", + "sha256:f3c19e5bd42bbe4bf345704ad7c326c74d3fd7a1b3844987853bef180be638d4" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==20.3.0" + }, + "txaio": { + "hashes": [ + "sha256:17938f2bca4a9cabce61346758e482ca4e600160cbc28e861493eac74a19539d", + "sha256:38a469daf93c37e5527cb062653d6393ae11663147c42fab7ddc3f6d00d434ae" + ], + "markers": "python_version >= '3.5'", + "version": "==20.4.1" + }, "tzlocal": { "hashes": [ "sha256:643c97c5294aedc737780a49d9df30889321cbe1204eac2c2ec6134035a92e44", @@ -815,6 +1089,66 @@ ], "markers": "python_version >= '3.6'", "version": "==3.4.0" + }, + "zope.interface": { + "hashes": [ + "sha256:05a97ba92c1c7c26f25c9f671aa1ef85ffead6cdad13770e5b689cf983adc7e1", + "sha256:07d61722dd7d85547b7c6b0f5486b4338001fab349f2ac5cabc0b7182eb3425d", + "sha256:09fc3922f235703c0b76f8234867685eee68a24a49fffa2220975f6142db45f1", + "sha256:0a990dcc97806e5980bbb54b2e46b9cde9e48932d8e6984daf71ef1745516123", + "sha256:150e8bcb7253a34a4535aeea3de36c0bb3b1a6a47a183a95d65a194b3e07f232", + "sha256:1743bcfe45af8846b775086471c28258f4c6e9ee8ef37484de4495f15a98b549", + "sha256:1b5f6c8fff4ed32aa2dd43e84061bc8346f32d3ba6ad6e58f088fe109608f102", + "sha256:21e49123f375703cf824214939d39df0af62c47d122d955b2a8d9153ea08cfd5", + "sha256:21f579134a47083ffb5ddd1307f0405c91aa8b61ad4be6fd5af0171474fe0c45", + "sha256:27c267dc38a0f0079e96a2945ee65786d38ef111e413c702fbaaacbab6361d00", + "sha256:299bde0ab9e5c4a92f01a152b7fbabb460f31343f1416f9b7b983167ab1e33bc", + "sha256:2ab88d8f228f803fcb8cb7d222c579d13dab2d3622c51e8cf321280da01102a7", + "sha256:2ced4c35061eea623bc84c7711eedce8ecc3c2c51cd9c6afa6290df3bae9e104", + "sha256:2dcab01c660983ba5e5a612e0c935141ccbee67d2e2e14b833e01c2354bd8034", + "sha256:32546af61a9a9b141ca38d971aa6eb9800450fa6620ce6323cc30eec447861f3", + "sha256:32b40a4c46d199827d79c86bb8cb88b1bbb764f127876f2cb6f3a47f63dbada3", + "sha256:3cc94c69f6bd48ed86e8e24f358cb75095c8129827df1298518ab860115269a4", + "sha256:42b278ac0989d6f5cf58d7e0828ea6b5951464e3cf2ff229dd09a96cb6ba0c86", + "sha256:495b63fd0302f282ee6c1e6ea0f1c12cb3d1a49c8292d27287f01845ff252a96", + "sha256:4af87cdc0d4b14e600e6d3d09793dce3b7171348a094ba818e2a68ae7ee67546", + "sha256:4b94df9f2fdde7b9314321bab8448e6ad5a23b80542dcab53e329527d4099dcb", + "sha256:4c48ddb63e2b20fba4c6a2bf81b4d49e99b6d4587fb67a6cd33a2c1f003af3e3", + "sha256:4df9afd17bd5477e9f8c8b6bb8507e18dd0f8b4efe73bb99729ff203279e9e3b", + "sha256:518950fe6a5d56f94ba125107895f938a4f34f704c658986eae8255edb41163b", + "sha256:538298e4e113ccb8b41658d5a4b605bebe75e46a30ceca22a5a289cf02c80bec", + "sha256:55465121e72e208a7b69b53de791402affe6165083b2ea71b892728bd19ba9ae", + "sha256:588384d70a0f19b47409cfdb10e0c27c20e4293b74fc891df3d8eb47782b8b3e", + "sha256:6278c080d4afffc9016e14325f8734456831124e8c12caa754fd544435c08386", + "sha256:64ea6c221aeee4796860405e1aedec63424cda4202a7ad27a5066876db5b0fd2", + "sha256:681dbb33e2b40262b33fd383bae63c36d33fd79fa1a8e4092945430744ffd34a", + "sha256:6936aa9da390402d646a32a6a38d5409c2d2afb2950f045a7d02ab25a4e7d08d", + "sha256:778d0ec38bbd288b150a3ae363c8ffd88d2207a756842495e9bffd8a8afbc89a", + "sha256:8251f06a77985a2729a8bdbefbae79ee78567dddc3acbd499b87e705ca59fe24", + "sha256:83b4aa5344cce005a9cff5d0321b2e318e871cc1dfc793b66c32dd4f59e9770d", + "sha256:844fad925ac5c2ad4faaceb3b2520ad016b5280105c6e16e79838cf951903a7b", + "sha256:8ceb3667dd13b8133f2e4d637b5b00f240f066448e2aa89a41f4c2d78a26ce50", + "sha256:92dc0fb79675882d0b6138be4bf0cec7ea7c7eede60aaca78303d8e8dbdaa523", + "sha256:974f5957e66a7524ea81df7b2686a456bfaf0408dbb7353ddfbedb594eadfef6", + "sha256:9789bd945e9f5bd026ed3f5b453d640befb8b1fc33a779c1fe8d3eb21fe3fb4a", + "sha256:a2b6d6eb693bc2fc6c484f2e5d93bd0b0da803fa77bf974f160533e555e4d095", + "sha256:aab9f1e34d810feb00bf841993552b8fcc6ae71d473c505381627143d0018a6a", + "sha256:abb61afd84f23099ac6099d804cdba9bd3b902aaaded3ffff47e490b0a495520", + "sha256:adf9ee115ae8ff8b6da4b854b4152f253b390ba64407a22d75456fe07dcbda65", + "sha256:aedc6c672b351afe6dfe17ff83ee5e7eb6ed44718f879a9328a68bdb20b57e11", + "sha256:b7a00ecb1434f8183395fac5366a21ee73d14900082ca37cf74993cf46baa56c", + "sha256:ba32f4a91c1cb7314c429b03afbf87b1fff4fb1c8db32260e7310104bd77f0c7", + "sha256:cbd0f2cbd8689861209cd89141371d3a22a11613304d1f0736492590aa0ab332", + "sha256:e4bc372b953bf6cec65a8d48482ba574f6e051621d157cf224227dbb55486b1e", + "sha256:eccac3d9aadc68e994b6d228cb0c8919fc47a5350d85a1b4d3d81d1e98baf40c", + "sha256:efd550b3da28195746bb43bd1d815058181a7ca6d9d6aa89dd37f5eefe2cacb7", + "sha256:efef581c8ba4d990770875e1a2218e856849d32ada2680e53aebc5d154a17e20", + "sha256:f057897711a630a0b7a6a03f1acf379b6ba25d37dc5dc217a97191984ba7f2fc", + "sha256:f37d45fab14ffef9d33a0dc3bc59ce0c5313e2253323312d47739192da94f5fd", + "sha256:f44906f70205d456d503105023041f1e63aece7623b31c390a0103db4de17537" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==5.2.0" } }, "develop": { @@ -980,7 +1314,6 @@ "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" }, "imagesize": { From e2456f4b3fb586edcb7794915b258fb4aa118489 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Mon, 7 Dec 2020 12:44:23 +0100 Subject: [PATCH 0016/1708] changes --- .../consumer-status-widget.component.html | 16 +++++----- .../saved-view-widget.component.css | 0 .../statistics-widget.component.css | 0 .../upload-file-widget.component.ts | 2 +- .../document-list/document-list.component.ts | 2 +- src/documents/consumer.py | 29 +++++++++---------- 6 files changed, 25 insertions(+), 24 deletions(-) delete mode 100644 src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.css delete mode 100644 src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.css diff --git a/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.html b/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.html index ff2117729..9ea715043 100644 --- a/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.html +++ b/src-ui/src/app/components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component.html @@ -1,10 +1,12 @@ <app-widget-frame title="Document consumer status"> - <div class="mb-2 border-bottom" *ngFor="let s of getStatus()"> - <div class="mb-1"><strong>{{s.filename}}:</strong> {{s.message}}</div> - <ngb-progressbar [type]="getType(s.status)" [value]="s.current_progress" [max]="s.max_progress" class="mb-2"></ngb-progressbar> - <div *ngIf="isFinished(s)" class="mb-2"> - <button *ngIf="s.document_id" class="btn btn-sm btn-outline-primary mr-2" routerLink="/documents/{{s.document_id}}" (click)="dismiss(s)">Open document</button> - <button class="btn btn-sm btn-outline-secondary" (click)="dismiss(s)">Dismiss</button> + <ng-container content> + <div class="mb-2 border-bottom" *ngFor="let s of getStatus()"> + <div class="mb-1"><strong>{{s.filename}}:</strong> {{s.message}}</div> + <ngb-progressbar [type]="getType(s.status)" [value]="s.current_progress" [max]="s.max_progress" class="mb-2"></ngb-progressbar> + <div *ngIf="isFinished(s)" class="mb-2"> + <button *ngIf="s.document_id" class="btn btn-sm btn-outline-primary mr-2" routerLink="/documents/{{s.document_id}}" (click)="dismiss(s)">Open document</button> + <button class="btn btn-sm btn-outline-secondary" (click)="dismiss(s)">Dismiss</button> + </div> </div> - </div> + </ng-container> </app-widget-frame> diff --git a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.css b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.css b/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/src-ui/src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.ts b/src-ui/src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.ts index 97b4ffee8..16a220229 100644 --- a/src-ui/src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.ts +++ b/src-ui/src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.ts @@ -29,7 +29,7 @@ export class UploadFileWidgetComponent implements OnInit { const formData = new FormData() formData.append('document', file, file.name) this.documentService.uploadDocument(formData).subscribe(result => { - this.toastService.showInfo(The document has been uploaded and will be processed by the consumer shortly.") + this.toastService.showInfo("The document has been uploaded and will be processed by the consumer shortly.") }, error => { switch (error.status) { case 400: { diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index fe6c8a894..4dc986d51 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -76,7 +76,7 @@ export class DocumentListComponent implements OnInit { saveViewConfig() { this.savedViewConfigService.updateConfig(this.list.savedView) - this.toastService.showToast(Toast.make("Information", `View "${this.list.savedView.title}" saved successfully.`)) + this.toastService.showInfo(`View "${this.list.savedView.title}" saved successfully.`) } saveViewConfigAs() { diff --git a/src/documents/consumer.py b/src/documents/consumer.py index b57e81d06..30c57e6a0 100755 --- a/src/documents/consumer.py +++ b/src/documents/consumer.py @@ -43,6 +43,11 @@ class Consumer(LoggingMixin): {'type': 'status_update', 'data': payload}) + def _fail(self, message): + self._send_progress(self.filename, 100, 100, 'FAILED', + message) + raise ConsumerError(f"{self.filename}: {message}") + def __init__(self): super().__init__() self.path = None @@ -56,8 +61,7 @@ class Consumer(LoggingMixin): def pre_check_file_exists(self): if not os.path.isfile(self.path): - raise ConsumerError("Cannot consume {}: It is not a file".format( - self.path)) + self._fail("File not found") def pre_check_duplicate(self): with open(self.path, "rb") as f: @@ -65,9 +69,7 @@ class Consumer(LoggingMixin): if Document.objects.filter(Q(checksum=checksum) | Q(archive_checksum=checksum)).exists(): # NOQA: E501 if settings.CONSUMER_DELETE_DUPLICATES: os.unlink(self.path) - raise ConsumerError( - "Not consuming {}: It is a duplicate.".format(self.filename) - ) + self._fail("Document is a duplicate") def pre_check_directories(self): os.makedirs(settings.SCRATCH_DIR, exist_ok=True) @@ -93,6 +95,9 @@ class Consumer(LoggingMixin): self.override_document_type_id = override_document_type_id self.override_tag_ids = override_tag_ids + self._send_progress(self.filename, 0, 100, 'WORKING', + 'Received new file.') + # this is for grouping logging entries for this particular file # together. @@ -112,7 +117,7 @@ class Consumer(LoggingMixin): parser_class = get_parser_class_for_mime_type(mime_type) if not parser_class: - raise ConsumerError(f"No parsers abvailable for {self.filename}") + self._fail("No parsers abvailable") else: self.log("debug", f"Parser: {parser_class.__name__} " @@ -120,8 +125,6 @@ class Consumer(LoggingMixin): # Notify all listeners that we're going to do some work. - self._send_progress(self.filename, 0, 100, 'WORKING', 'Consumption started') - document_consumption_started.send( sender=self.__class__, filename=self.path, @@ -130,7 +133,7 @@ class Consumer(LoggingMixin): def progress_callback(current_progress, max_progress, message): # recalculate progress to be within 20 and 80 - p = int((current_progress / max_progress) * 60 + 20) + p = int((current_progress / max_progress) * 50 + 20) self._send_progress(self.filename, p, 100, "WORKING", message) # This doesn't parse the document yet, but gives us a parser. @@ -167,9 +170,7 @@ class Consumer(LoggingMixin): self.log( "error", f"Error while consuming document {self.filename}: {e}") - self._send_progress(self.filename, 100, 100, 'FAILED', - "Failed: {}".format(e)) - raise ConsumerError(e) + self._fail(e) # Prepare the document classifier. @@ -246,9 +247,7 @@ class Consumer(LoggingMixin): f"The following error occured while consuming " f"{self.filename}: {e}" ) - self._send_progress(self.filename, 100, 100, 'FAILED', - "Failed: {}".format(e)) - raise ConsumerError(e) + self._fail(str(e)) finally: document_parser.cleanup() From 72706a335da809122c886576f50df4528c15c75f Mon Sep 17 00:00:00 2001 From: Jonas Winkler <dev@jpwinkler.de> Date: Sun, 6 Dec 2020 23:30:51 +0100 Subject: [PATCH 0017/1708] Update CONTRIBUTING.md --- CONTRIBUTING.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bd6080d35..a8fb1f8e9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,3 +24,7 @@ feature-X branches is for experimental stuff that will eventually be merged into I'm trying to get most of paperless tested, so please do the same for your code! I know its a hassle, but it makes sure that your code works now and will allow us to detect regressions easily. To test your code, execute `pytest` in the src/ directory. Executing that in the project root is no good. This also generates a html coverage report, which you can use to see if you missed anything important during testing. + +## More info: + +... is available in the documentation. https://paperless-ng.readthedocs.io/en/latest/extending.html From f3fd0fcf72f8009e9b67cf18319e37100a97f0bb Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Tue, 8 Dec 2020 23:08:02 -0800 Subject: [PATCH 0018/1708] Basic tags, correspondents & document type dropdowns --- src-ui/src/app/app.module.ts | 4 +- .../document-list.component.html | 69 +++++++++++++++++-- .../document-list/document-list.component.ts | 30 ++++++-- src-ui/src/app/pipes/filter.pipe.ts | 17 +++++ 4 files changed, 107 insertions(+), 13 deletions(-) create mode 100644 src-ui/src/app/pipes/filter.pipe.ts diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index ad12c9c47..af2c46492 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -48,6 +48,7 @@ import { WidgetFrameComponent } from './components/dashboard/widgets/widget-fram import { WelcomeWidgetComponent } from './components/dashboard/widgets/welcome-widget/welcome-widget.component'; import { YesNoPipe } from './pipes/yes-no.pipe'; import { FileSizePipe } from './pipes/file-size.pipe'; +import { FilterPipe } from './pipes/filter.pipe'; @NgModule({ declarations: [ @@ -88,7 +89,8 @@ import { FileSizePipe } from './pipes/file-size.pipe'; WidgetFrameComponent, WelcomeWidgetComponent, YesNoPipe, - FileSizePipe + FileSizePipe, + FilterPipe ], imports: [ BrowserModule, diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index 8608ed92b..886b5832a 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -1,5 +1,4 @@ <app-page-header [title]="getTitle()"> - <div class="btn-group btn-group-toggle" ngbRadioGroup [(ngModel)]="displayMode" (ngModelChange)="saveDisplayMode()"> <label ngbButtonLabel class="btn-outline-primary btn-sm"> @@ -21,6 +20,7 @@ </svg> </label> </div> + <div class="btn-group btn-group-toggle ml-2" ngbRadioGroup [(ngModel)]="list.sortDirection"> <div ngbDropdown class="btn-group"> <button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle>Sort by</button> @@ -42,13 +42,14 @@ </svg> </label> </div> + <div class="btn-group ml-2"> <button type="button" class="btn btn-sm" [ngClass]="isFiltered ? 'btn-primary' : 'btn-outline-primary'" (click)="showFilter=!showFilter"> <svg class="toolbaricon" fill="currentColor"> <use xlink:href="assets/bootstrap-icons.svg#funnel" /> </svg> - Filter + Advanced Filters </button> <div class="btn-group" ngbDropdown role="group"> @@ -58,18 +59,69 @@ <button ngbDropdownItem *ngFor="let config of savedViewConfigService.getConfigs()" (click)="loadViewConfig(config)">{{config.title}}</button> <div class="dropdown-divider" *ngIf="savedViewConfigService.getConfigs().length > 0"></div> </ng-container> - + <button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.savedViewId">Save "{{list.savedViewTitle}}"</button> <button ngbDropdownItem (click)="saveViewConfigAs()">Save as...</button> </div> </div> </div> + </app-page-header> +<div class="row pb-1 mb-3 align-items-right" > + <div class="btn-toolbar col-auto"> + <div class="btn-group ml-2" ngbDropdown role="group"> + <button class="btn btn-outline-primary btn-sm" id="dropdownTags" ngbDropdownToggle>Tags</button> + <div class="dropdown-menu" ngbDropdownMenu aria-labelledby="dropdownTags"> + <div class="list-group list-group-flush"> + <input class="list-group-item form-control" type="text" [(ngModel)]="searchText" placeholder="Filter tags"> + <ng-container *ngIf="(tags | filter: searchText).length > 0"> + <button class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" role="menuitem" *ngFor="let tag of tags | filter: searchText; let i = index" (click)="filterByTag(tag.id, true)"> + {{tag.name}} + <span class="badge bg-primary text-light rounded-pill">{{tag.document_count}}</span> + </button> + </ng-container> + </div> + </div> + </div> + + <div class="btn-group ml-2" ngbDropdown role="group"> + <button class="btn btn-outline-primary btn-sm" id="dropdownCorrespondents" ngbDropdownToggle>Correspondents</button> + <div class="dropdown-menu" ngbDropdownMenu aria-labelledby="dropdownCorrespondents"> + <div class="list-group list-group-flush"> + <input class="list-group-item form-control" type="text" [(ngModel)]="searchText" placeholder="Filter correspondents"> + <ng-container *ngIf="(correspondents | filter: searchText).length > 0"> + <button class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" role="menuitem" *ngFor="let correspondent of correspondents | filter: searchText; let i = index" (click)="filterByCorrespondent(correspondent.id, true)"> + {{correspondent.name}} + <span class="badge bg-primary text-light rounded-pill">{{correspondent.document_count}}</span> + </button> + </ng-container> + </div> + </div> + </div> + + <div class="btn-group ml-2" ngbDropdown role="group"> + <button class="btn btn-outline-primary btn-sm" id="dropdownDocumentTypes" ngbDropdownToggle>Document Types</button> + <div class="dropdown-menu" ngbDropdownMenu aria-labelledby="dropdownDocumentTypes"> + <div class="list-group list-group-flush"> + <input class="list-group-item form-control" type="text" [(ngModel)]="searchText" placeholder="Filter tags"> + <ng-container *ngIf="(documentTypes | filter: searchText).length > 0"> + <button class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" role="menuitem" *ngFor="let documentType of documentTypes | filter: searchText; let i = index" (click)="filterByDocumentType(documentType.id, true)"> + {{documentType.name}} + <span class="badge bg-primary text-light rounded-pill">{{documentType.document_count}}</span> + </button> + </ng-container> + </div> + </div> + </div> + + </div> +</div> + <div class="card w-100 mb-3" [hidden]="!showFilter"> <div class="card-body"> - <h5 class="card-title">Filter</h5> + <h5 class="card-title">Advanced Filters</h5> <app-filter-editor [(filterRules)]="filterRules" (apply)="applyFilterRules()" (clear)="clearFilterRules()"></app-filter-editor> </div> </div> @@ -125,5 +177,12 @@ <div class=" m-n2 row" *ngIf="displayMode == 'smallCards'"> - <app-document-card-small [document]="d" *ngFor="let d of list.documents" (clickTag)="filterByTag($event)" (clickCorrespondent)="filterByCorrespondent($event)"></app-document-card-small> + <app-document-card-small [document]="d" *ngFor="let d of list.documents" (clickTag)="filterByTag($event)" (clickCorrespondent)="filterByCorrespondent($event)"></app-document-card-small> </div> + +<script type="text/ng-template" id="customTemplate.html"> + <a> + <img ng-src="http://upload.wikimedia.org/wikipedia/commons/thumb/{{match.model.flag}}" width="16"> + <span ng-bind-html="match.label | uibTypeaheadHighlight:query"></span> + </a> +</script> diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index 09e73dd96..9870f3dc1 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -11,6 +11,12 @@ import { SavedViewConfigService } from 'src/app/services/saved-view-config.servi import { Toast, ToastService } from 'src/app/services/toast.service'; import { environment } from 'src/environments/environment'; import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component'; +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'; @Component({ selector: 'app-document-list', @@ -25,13 +31,20 @@ export class DocumentListComponent implements OnInit { public route: ActivatedRoute, private toastService: ToastService, public modalService: NgbModal, - private titleService: Title) { } + private titleService: Title, + private tagService: TagService, + private correspondentService: CorrespondentService, + private documentTypeService: DocumentTypeService) { } displayMode = 'smallCards' // largeCards, smallCards, details filterRules: FilterRule[] = [] showFilter = false + tags: PaperlessTag[] = [] + correspondents: PaperlessCorrespondent[] = [] + documentTypes: PaperlessDocumentType[] = [] + get isFiltered() { return this.list.filterRules?.length > 0 } @@ -67,6 +80,9 @@ export class DocumentListComponent implements OnInit { this.list.clear() this.list.reload() }) + 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) } applyFilterRules() { @@ -103,8 +119,8 @@ export class DocumentListComponent implements OnInit { }) } - filterByTag(tag_id: number) { - let filterRules = this.list.filterRules + filterByTag(tag_id: number, singleton: boolean = false) { + let filterRules = singleton ? [] : this.list.filterRules if (filterRules.find(rule => rule.type.id == FILTER_HAS_TAG && rule.value == tag_id)) { return } @@ -114,8 +130,8 @@ export class DocumentListComponent implements OnInit { this.applyFilterRules() } - filterByCorrespondent(correspondent_id: number) { - let filterRules = this.list.filterRules + filterByCorrespondent(correspondent_id: number, singleton: boolean = false) { + let filterRules = singleton ? [] : this.list.filterRules let existing_rule = filterRules.find(rule => rule.type.id == FILTER_CORRESPONDENT) if (existing_rule && existing_rule.value == correspondent_id) { return @@ -128,8 +144,8 @@ export class DocumentListComponent implements OnInit { this.applyFilterRules() } - filterByDocumentType(document_type_id: number) { - let filterRules = this.list.filterRules + filterByDocumentType(document_type_id: number, singleton: boolean = false) { + let filterRules = singleton ? [] : this.list.filterRules let existing_rule = filterRules.find(rule => rule.type.id == FILTER_DOCUMENT_TYPE) if (existing_rule && existing_rule.value == document_type_id) { return diff --git a/src-ui/src/app/pipes/filter.pipe.ts b/src-ui/src/app/pipes/filter.pipe.ts new file mode 100644 index 000000000..f799f40cc --- /dev/null +++ b/src-ui/src/app/pipes/filter.pipe.ts @@ -0,0 +1,17 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'filter' +}) +export class FilterPipe implements PipeTransform { + transform(items: any[], searchText: string): any[] { + if (!items) return []; + if (!searchText) return items; + + return items.filter(item => { + return Object.keys(item).some(key => { + return String(item[key]).toLowerCase().includes(searchText.toLowerCase()); + }); + }); + } +} From 23ba3be68ff56f54abe8bba430d8fafa8ee25840 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Tue, 8 Dec 2020 23:39:38 -0800 Subject: [PATCH 0019/1708] Toggling of items --- .../document-list.component.html | 15 ++++- .../document-list/document-list.component.ts | 64 +++++++++++++++++-- 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index 886b5832a..7c89517be 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -77,7 +77,10 @@ <div class="list-group list-group-flush"> <input class="list-group-item form-control" type="text" [(ngModel)]="searchText" placeholder="Filter tags"> <ng-container *ngIf="(tags | filter: searchText).length > 0"> - <button class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" role="menuitem" *ngFor="let tag of tags | filter: searchText; let i = index" (click)="filterByTag(tag.id, true)"> + <button class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" role="menuitem" *ngFor="let tag of tags | filter: searchText; let i = index" (click)="toggleFilterByTag(tag.id)"> + <svg *ngIf="currentViewIncludesTag(tag.id)" 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> {{tag.name}} <span class="badge bg-primary text-light rounded-pill">{{tag.document_count}}</span> </button> @@ -92,7 +95,10 @@ <div class="list-group list-group-flush"> <input class="list-group-item form-control" type="text" [(ngModel)]="searchText" placeholder="Filter correspondents"> <ng-container *ngIf="(correspondents | filter: searchText).length > 0"> - <button class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" role="menuitem" *ngFor="let correspondent of correspondents | filter: searchText; let i = index" (click)="filterByCorrespondent(correspondent.id, true)"> + <button class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" role="menuitem" *ngFor="let correspondent of correspondents | filter: searchText; let i = index" (click)="toggleFilterByCorrespondent(correspondent.id)"> + <svg *ngIf="currentViewIncludesCorrespondent(correspondent.id)" 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> {{correspondent.name}} <span class="badge bg-primary text-light rounded-pill">{{correspondent.document_count}}</span> </button> @@ -107,7 +113,10 @@ <div class="list-group list-group-flush"> <input class="list-group-item form-control" type="text" [(ngModel)]="searchText" placeholder="Filter tags"> <ng-container *ngIf="(documentTypes | filter: searchText).length > 0"> - <button class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" role="menuitem" *ngFor="let documentType of documentTypes | filter: searchText; let i = index" (click)="filterByDocumentType(documentType.id, true)"> + <button class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" role="menuitem" *ngFor="let documentType of documentTypes | filter: searchText; let i = index" (click)="toggleFilterByDocumentType(documentType.id)"> + <svg *ngIf="currentViewIncludesDocumentType(documentType.id)" 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> {{documentType.name}} <span class="badge bg-primary text-light rounded-pill">{{documentType.document_count}}</span> </button> diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index 9870f3dc1..5e5134dc7 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -119,8 +119,8 @@ export class DocumentListComponent implements OnInit { }) } - filterByTag(tag_id: number, singleton: boolean = false) { - let filterRules = singleton ? [] : this.list.filterRules + filterByTag(tag_id: number) { + let filterRules = this.list.filterRules if (filterRules.find(rule => rule.type.id == FILTER_HAS_TAG && rule.value == tag_id)) { return } @@ -130,8 +130,8 @@ export class DocumentListComponent implements OnInit { this.applyFilterRules() } - filterByCorrespondent(correspondent_id: number, singleton: boolean = false) { - let filterRules = singleton ? [] : this.list.filterRules + filterByCorrespondent(correspondent_id: number) { + let filterRules = this.list.filterRules let existing_rule = filterRules.find(rule => rule.type.id == FILTER_CORRESPONDENT) if (existing_rule && existing_rule.value == correspondent_id) { return @@ -144,8 +144,8 @@ export class DocumentListComponent implements OnInit { this.applyFilterRules() } - filterByDocumentType(document_type_id: number, singleton: boolean = false) { - let filterRules = singleton ? [] : this.list.filterRules + filterByDocumentType(document_type_id: number) { + let filterRules = this.list.filterRules let existing_rule = filterRules.find(rule => rule.type.id == FILTER_DOCUMENT_TYPE) if (existing_rule && existing_rule.value == document_type_id) { return @@ -158,4 +158,56 @@ export class DocumentListComponent implements OnInit { this.applyFilterRules() } + findRuleIndex(type_id: number, value: any) { + return this.list.filterRules.findIndex(rule => rule.type.id == type_id && rule.value == value) + } + + toggleFilterByTag(tag_id: number) { + let existingRuleIndex = this.findRuleIndex(FILTER_HAS_TAG, tag_id) + if (existingRuleIndex !== -1) { + let filterRules = this.list.filterRules + filterRules.splice(existingRuleIndex, 1) + this.filterRules = filterRules + this.applyFilterRules() + } else { + this.filterByTag(tag_id) + } + } + + toggleFilterByCorrespondent(correspondent_id: number) { + let existingRuleIndex = this.findRuleIndex(FILTER_CORRESPONDENT, correspondent_id) + if (existingRuleIndex !== -1) { + let filterRules = this.list.filterRules + filterRules.splice(existingRuleIndex, 1) + this.filterRules = filterRules + this.applyFilterRules() + } else { + this.filterByCorrespondent(correspondent_id) + } + } + + toggleFilterByDocumentType(document_type_id: number) { + let existingRuleIndex = this.findRuleIndex(FILTER_DOCUMENT_TYPE, document_type_id) + if (existingRuleIndex !== -1) { + let filterRules = this.list.filterRules + filterRules.splice(existingRuleIndex, 1) + this.filterRules = filterRules + this.applyFilterRules() + } else { + this.filterByDocumentType(document_type_id) + } + } + + currentViewIncludesTag(tag_id: number) { + return this.findRuleIndex(FILTER_HAS_TAG, tag_id) !== -1 + } + + currentViewIncludesCorrespondent(correspondent_id: number) { + return this.findRuleIndex(FILTER_CORRESPONDENT, correspondent_id) !== -1 + } + + currentViewIncludesDocumentType(document_type_id: number) { + return this.findRuleIndex(FILTER_DOCUMENT_TYPE, document_type_id) !== -1 + } + } From da87542a5204db3244f2c270ac1ee82394110d71 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Tue, 8 Dec 2020 23:53:19 -0800 Subject: [PATCH 0020/1708] Change advanced to show / hide --- .../components/document-list/document-list.component.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index 7c89517be..58fbbcbe8 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -49,13 +49,13 @@ <svg class="toolbaricon" fill="currentColor"> <use xlink:href="assets/bootstrap-icons.svg#funnel" /> </svg> - Advanced Filters + {{ showFilter ? 'Hide' : 'Show' }} Filter Editor </button> <div class="btn-group" ngbDropdown role="group"> <button class="btn btn-sm btn-outline-primary dropdown-toggle-split" ngbDropdownToggle></button> <div class="dropdown-menu" ngbDropdownMenu> - <ng-container *ngIf="!list.savedViewId" > + <ng-container *ngIf="!list.savedViewId"> <button ngbDropdownItem *ngFor="let config of savedViewConfigService.getConfigs()" (click)="loadViewConfig(config)">{{config.title}}</button> <div class="dropdown-divider" *ngIf="savedViewConfigService.getConfigs().length > 0"></div> </ng-container> @@ -69,7 +69,7 @@ </app-page-header> -<div class="row pb-1 mb-3 align-items-right" > +<div class="row pb-1 mb-3 align-items-right"> <div class="btn-toolbar col-auto"> <div class="btn-group ml-2" ngbDropdown role="group"> <button class="btn btn-outline-primary btn-sm" id="dropdownTags" ngbDropdownToggle>Tags</button> @@ -130,7 +130,7 @@ <div class="card w-100 mb-3" [hidden]="!showFilter"> <div class="card-body"> - <h5 class="card-title">Advanced Filters</h5> + <h5 class="card-title">Filter Editor</h5> <app-filter-editor [(filterRules)]="filterRules" (apply)="applyFilterRules()" (clear)="clearFilterRules()"></app-filter-editor> </div> </div> From c28f19c9cfbcda0acd34a50ed5d28c74eb3ae974 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Tue, 8 Dec 2020 23:53:56 -0800 Subject: [PATCH 0021/1708] Quick filter styling --- .../components/document-list/document-list.component.html | 6 +++--- .../components/document-list/document-list.component.scss | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index 58fbbcbe8..f94c2be2c 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -73,7 +73,7 @@ <div class="btn-toolbar col-auto"> <div class="btn-group ml-2" ngbDropdown role="group"> <button class="btn btn-outline-primary btn-sm" id="dropdownTags" ngbDropdownToggle>Tags</button> - <div class="dropdown-menu" ngbDropdownMenu aria-labelledby="dropdownTags"> + <div class="dropdown-menu quick-filter" ngbDropdownMenu aria-labelledby="dropdownTags"> <div class="list-group list-group-flush"> <input class="list-group-item form-control" type="text" [(ngModel)]="searchText" placeholder="Filter tags"> <ng-container *ngIf="(tags | filter: searchText).length > 0"> @@ -91,7 +91,7 @@ <div class="btn-group ml-2" ngbDropdown role="group"> <button class="btn btn-outline-primary btn-sm" id="dropdownCorrespondents" ngbDropdownToggle>Correspondents</button> - <div class="dropdown-menu" ngbDropdownMenu aria-labelledby="dropdownCorrespondents"> + <div class="dropdown-menu quick-filter" ngbDropdownMenu aria-labelledby="dropdownCorrespondents"> <div class="list-group list-group-flush"> <input class="list-group-item form-control" type="text" [(ngModel)]="searchText" placeholder="Filter correspondents"> <ng-container *ngIf="(correspondents | filter: searchText).length > 0"> @@ -109,7 +109,7 @@ <div class="btn-group ml-2" ngbDropdown role="group"> <button class="btn btn-outline-primary btn-sm" id="dropdownDocumentTypes" ngbDropdownToggle>Document Types</button> - <div class="dropdown-menu" ngbDropdownMenu aria-labelledby="dropdownDocumentTypes"> + <div class="dropdown-menu quick-filter" ngbDropdownMenu aria-labelledby="dropdownDocumentTypes"> <div class="list-group list-group-flush"> <input class="list-group-item form-control" type="text" [(ngModel)]="searchText" placeholder="Filter tags"> <ng-container *ngIf="(documentTypes | filter: searchText).length > 0"> diff --git a/src-ui/src/app/components/document-list/document-list.component.scss b/src-ui/src/app/components/document-list/document-list.component.scss index e69de29bb..2d6fc29ef 100644 --- a/src-ui/src/app/components/document-list/document-list.component.scss +++ b/src-ui/src/app/components/document-list/document-list.component.scss @@ -0,0 +1,4 @@ +.quick-filter { + min-width: 250px; + max-height: 400px; +} From 06a3fff2bc41edfe9b1bc204e0e09d95ce2fb963 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Tue, 8 Dec 2020 23:56:27 -0800 Subject: [PATCH 0022/1708] Refactor clashing filter variable --- .../document-list/document-list.component.html | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index f94c2be2c..672f7c9af 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -75,9 +75,9 @@ <button class="btn btn-outline-primary btn-sm" id="dropdownTags" ngbDropdownToggle>Tags</button> <div class="dropdown-menu quick-filter" ngbDropdownMenu aria-labelledby="dropdownTags"> <div class="list-group list-group-flush"> - <input class="list-group-item form-control" type="text" [(ngModel)]="searchText" placeholder="Filter tags"> - <ng-container *ngIf="(tags | filter: searchText).length > 0"> - <button class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" role="menuitem" *ngFor="let tag of tags | filter: searchText; let i = index" (click)="toggleFilterByTag(tag.id)"> + <input class="list-group-item form-control" type="text" [(ngModel)]="filterTagsText" placeholder="Filter tags"> + <ng-container *ngIf="(tags | filter: filterTagsText).length > 0"> + <button class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" role="menuitem" *ngFor="let tag of tags | filter: filterTagsText; let i = index" (click)="toggleFilterByTag(tag.id)"> <svg *ngIf="currentViewIncludesTag(tag.id)" 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> @@ -93,9 +93,9 @@ <button class="btn btn-outline-primary btn-sm" id="dropdownCorrespondents" ngbDropdownToggle>Correspondents</button> <div class="dropdown-menu quick-filter" ngbDropdownMenu aria-labelledby="dropdownCorrespondents"> <div class="list-group list-group-flush"> - <input class="list-group-item form-control" type="text" [(ngModel)]="searchText" placeholder="Filter correspondents"> - <ng-container *ngIf="(correspondents | filter: searchText).length > 0"> - <button class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" role="menuitem" *ngFor="let correspondent of correspondents | filter: searchText; let i = index" (click)="toggleFilterByCorrespondent(correspondent.id)"> + <input class="list-group-item form-control" type="text" [(ngModel)]="filterCorrespondentsText" placeholder="Filter correspondents"> + <ng-container *ngIf="(correspondents | filter: filterCorrespondentsText).length > 0"> + <button class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" role="menuitem" *ngFor="let correspondent of correspondents | filter: filterCorrespondentsText; let i = index" (click)="toggleFilterByCorrespondent(correspondent.id)"> <svg *ngIf="currentViewIncludesCorrespondent(correspondent.id)" 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> @@ -111,9 +111,9 @@ <button class="btn btn-outline-primary btn-sm" id="dropdownDocumentTypes" ngbDropdownToggle>Document Types</button> <div class="dropdown-menu quick-filter" ngbDropdownMenu aria-labelledby="dropdownDocumentTypes"> <div class="list-group list-group-flush"> - <input class="list-group-item form-control" type="text" [(ngModel)]="searchText" placeholder="Filter tags"> - <ng-container *ngIf="(documentTypes | filter: searchText).length > 0"> - <button class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" role="menuitem" *ngFor="let documentType of documentTypes | filter: searchText; let i = index" (click)="toggleFilterByDocumentType(documentType.id)"> + <input class="list-group-item form-control" type="text" [(ngModel)]="filterDocumentTypesText" placeholder="Filter tags"> + <ng-container *ngIf="(documentTypes | filter: filterDocumentTypesText).length > 0"> + <button class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" role="menuitem" *ngFor="let documentType of documentTypes | filter: filterDocumentTypesText; let i = index" (click)="toggleFilterByDocumentType(documentType.id)"> <svg *ngIf="currentViewIncludesDocumentType(documentType.id)" 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> From 0d48aea3087610ee45f470bc890f2f2f99f7e59f Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Tue, 8 Dec 2020 23:58:56 -0800 Subject: [PATCH 0023/1708] Label, visual tweaks --- .../app/components/document-list/document-list.component.html | 1 + 1 file changed, 1 insertion(+) diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index 672f7c9af..586ef8cfb 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -71,6 +71,7 @@ <div class="row pb-1 mb-3 align-items-right"> <div class="btn-toolbar col-auto"> + <span class="text-muted mt-1 mr-2">Quick Filters:</span> <div class="btn-group ml-2" ngbDropdown role="group"> <button class="btn btn-outline-primary btn-sm" id="dropdownTags" ngbDropdownToggle>Tags</button> <div class="dropdown-menu quick-filter" ngbDropdownMenu aria-labelledby="dropdownTags"> From 0f635d1bb2f51e6bbbe20cc4a00ec43e7ab9cfc2 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Wed, 9 Dec 2020 00:40:23 -0800 Subject: [PATCH 0024/1708] Clear button & visual tweaks --- .../document-list.component.html | 49 ++++++++++++------- .../document-list.component.scss | 5 ++ .../document-list/document-list.component.ts | 9 +++- 3 files changed, 44 insertions(+), 19 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index 586ef8cfb..c0f2332b4 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -78,12 +78,14 @@ <div class="list-group list-group-flush"> <input class="list-group-item form-control" type="text" [(ngModel)]="filterTagsText" placeholder="Filter tags"> <ng-container *ngIf="(tags | filter: filterTagsText).length > 0"> - <button class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" role="menuitem" *ngFor="let tag of tags | filter: filterTagsText; let i = index" (click)="toggleFilterByTag(tag.id)"> - <svg *ngIf="currentViewIncludesTag(tag.id)" 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> - {{tag.name}} - <span class="badge bg-primary text-light rounded-pill">{{tag.document_count}}</span> + <button class="list-group-item list-group-item-action d-flex align-items-center" role="menuitem" *ngFor="let tag of tags | filter: filterTagsText; let i = index" (click)="toggleFilterByTag(tag.id)"> + <div class="selected-icon mr-1"> + <svg *ngIf="currentViewIncludesTag(tag.id)" 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>{{tag.name}}</div> + <div class="badge bg-primary text-light rounded-pill ml-auto">{{tag.document_count}}</div> </button> </ng-container> </div> @@ -96,12 +98,14 @@ <div class="list-group list-group-flush"> <input class="list-group-item form-control" type="text" [(ngModel)]="filterCorrespondentsText" placeholder="Filter correspondents"> <ng-container *ngIf="(correspondents | filter: filterCorrespondentsText).length > 0"> - <button class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" role="menuitem" *ngFor="let correspondent of correspondents | filter: filterCorrespondentsText; let i = index" (click)="toggleFilterByCorrespondent(correspondent.id)"> - <svg *ngIf="currentViewIncludesCorrespondent(correspondent.id)" 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> - {{correspondent.name}} - <span class="badge bg-primary text-light rounded-pill">{{correspondent.document_count}}</span> + <button class="list-group-item list-group-item-action d-flex align-items-center" role="menuitem" *ngFor="let correspondent of correspondents | filter: filterCorrespondentsText; let i = index" (click)="toggleFilterByCorrespondent(correspondent.id)"> + <div class="selected-icon mr-1"> + <svg *ngIf="currentViewIncludesCorrespondent(correspondent.id)" 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>{{correspondent.name}}</div> + <div class="badge bg-primary text-light rounded-pill ml-auto">{{correspondent.document_count}}</div> </button> </ng-container> </div> @@ -114,18 +118,27 @@ <div class="list-group list-group-flush"> <input class="list-group-item form-control" type="text" [(ngModel)]="filterDocumentTypesText" placeholder="Filter tags"> <ng-container *ngIf="(documentTypes | filter: filterDocumentTypesText).length > 0"> - <button class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" role="menuitem" *ngFor="let documentType of documentTypes | filter: filterDocumentTypesText; let i = index" (click)="toggleFilterByDocumentType(documentType.id)"> - <svg *ngIf="currentViewIncludesDocumentType(documentType.id)" 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> - {{documentType.name}} - <span class="badge bg-primary text-light rounded-pill">{{documentType.document_count}}</span> + <button class="list-group-item list-group-item-action d-flex align-items-center" role="menuitem" *ngFor="let documentType of documentTypes | filter: filterDocumentTypesText; let i = index" (click)="toggleFilterByDocumentType(documentType.id)"> + <div class="selected-icon mr-1"> + <svg *ngIf="currentViewIncludesDocumentType(documentType.id)" 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>{{documentType.name}}</div> + <div class="badge bg-primary text-light rounded-pill ml-auto">{{documentType.document_count}}</div> </button> </ng-container> </div> </div> </div> + <button class="btn-link border-0 bg-transparent ml-3 text-muted" *ngIf="currentViewIncludesQuickFilter()" (click)="filterEditor.clearClicked()"> + <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 + </button> + </div> </div> diff --git a/src-ui/src/app/components/document-list/document-list.component.scss b/src-ui/src/app/components/document-list/document-list.component.scss index 2d6fc29ef..ee3736d3d 100644 --- a/src-ui/src/app/components/document-list/document-list.component.scss +++ b/src-ui/src/app/components/document-list/document-list.component.scss @@ -1,4 +1,9 @@ .quick-filter { min-width: 250px; max-height: 400px; + + .selected-icon { + min-width: 1em; + min-height: 1em; + } } diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index 5e5134dc7..22ee6ca2d 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, ViewChild } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; @@ -17,6 +17,7 @@ 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 { FilterEditorComponent } from 'src/app/components/filter-editor/filter-editor.component'; @Component({ selector: 'app-document-list', @@ -45,6 +46,8 @@ export class DocumentListComponent implements OnInit { correspondents: PaperlessCorrespondent[] = [] documentTypes: PaperlessDocumentType[] = [] + @ViewChild(FilterEditorComponent) filterEditor; + get isFiltered() { return this.list.filterRules?.length > 0 } @@ -210,4 +213,8 @@ export class DocumentListComponent implements OnInit { return this.findRuleIndex(FILTER_DOCUMENT_TYPE, document_type_id) !== -1 } + currentViewIncludesQuickFilter() { + return this.list.filterRules.find(rule => rule.type.id == FILTER_HAS_TAG || rule.type.id == FILTER_CORRESPONDENT || rule.type.id == FILTER_DOCUMENT_TYPE) !== undefined + } + } From 4fbb814e5bf822a624446911583fb4b1e26239c1 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Wed, 9 Dec 2020 01:25:04 -0800 Subject: [PATCH 0025/1708] Visual tweaks --- .../components/document-list/document-list.component.html | 6 +++--- .../components/document-list/document-list.component.scss | 3 ++- .../app/components/document-list/document-list.component.ts | 3 +++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index c0f2332b4..03ce515cd 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -84,7 +84,7 @@ <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>{{tag.name}}</div> + <div class="mr-1">{{tag.name}}</div> <div class="badge bg-primary text-light rounded-pill ml-auto">{{tag.document_count}}</div> </button> </ng-container> @@ -104,7 +104,7 @@ <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>{{correspondent.name}}</div> + <div class="mr-1">{{correspondent.name}}</div> <div class="badge bg-primary text-light rounded-pill ml-auto">{{correspondent.document_count}}</div> </button> </ng-container> @@ -124,7 +124,7 @@ <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>{{documentType.name}}</div> + <div class="mr-1">{{documentType.name}}</div> <div class="badge bg-primary text-light rounded-pill ml-auto">{{documentType.document_count}}</div> </button> </ng-container> diff --git a/src-ui/src/app/components/document-list/document-list.component.scss b/src-ui/src/app/components/document-list/document-list.component.scss index ee3736d3d..2513a1adc 100644 --- a/src-ui/src/app/components/document-list/document-list.component.scss +++ b/src-ui/src/app/components/document-list/document-list.component.scss @@ -1,7 +1,8 @@ .quick-filter { min-width: 250px; max-height: 400px; - + overflow-y: scroll; + .selected-icon { min-width: 1em; min-height: 1em; diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index 22ee6ca2d..91ccfb082 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -45,6 +45,9 @@ export class DocumentListComponent implements OnInit { tags: PaperlessTag[] = [] correspondents: PaperlessCorrespondent[] = [] documentTypes: PaperlessDocumentType[] = [] + filterTagsText: string + filterCorrespondentsText: string + filterDocumentTypesText: string @ViewChild(FilterEditorComponent) filterEditor; From a4f7c5ddcb77028bc95cc5b80adde837f0ef2cb6 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Wed, 9 Dec 2020 01:34:09 -0800 Subject: [PATCH 0026/1708] Unused test code --- .../components/document-list/document-list.component.html | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index 03ce515cd..767d207ae 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -202,10 +202,3 @@ <div class=" m-n2 row" *ngIf="displayMode == 'smallCards'"> <app-document-card-small [document]="d" *ngFor="let d of list.documents" (clickTag)="filterByTag($event)" (clickCorrespondent)="filterByCorrespondent($event)"></app-document-card-small> </div> - -<script type="text/ng-template" id="customTemplate.html"> - <a> - <img ng-src="http://upload.wikimedia.org/wikipedia/commons/thumb/{{match.model.flag}}" width="16"> - <span ng-bind-html="match.label | uibTypeaheadHighlight:query"></span> - </a> -</script> From ed236460b5bd0d3e2cd0f2de342ae547e90e4931 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Wed, 9 Dec 2020 01:36:33 -0800 Subject: [PATCH 0027/1708] Fix document type search field placeholder --- .../app/components/document-list/document-list.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index 767d207ae..e36aa3571 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -116,7 +116,7 @@ <button class="btn btn-outline-primary btn-sm" id="dropdownDocumentTypes" ngbDropdownToggle>Document Types</button> <div class="dropdown-menu quick-filter" ngbDropdownMenu aria-labelledby="dropdownDocumentTypes"> <div class="list-group list-group-flush"> - <input class="list-group-item form-control" type="text" [(ngModel)]="filterDocumentTypesText" placeholder="Filter tags"> + <input class="list-group-item form-control" type="text" [(ngModel)]="filterDocumentTypesText" placeholder="Filter document types"> <ng-container *ngIf="(documentTypes | filter: filterDocumentTypesText).length > 0"> <button class="list-group-item list-group-item-action d-flex align-items-center" role="menuitem" *ngFor="let documentType of documentTypes | filter: filterDocumentTypesText; let i = index" (click)="toggleFilterByDocumentType(documentType.id)"> <div class="selected-icon mr-1"> From f0d86130eca3fb926b2e255cdea454acc5723883 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Wed, 9 Dec 2020 01:52:44 -0800 Subject: [PATCH 0028/1708] Use tag component for tag colors etc --- .../app/components/document-list/document-list.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index e36aa3571..96e13f935 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -84,7 +84,7 @@ <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">{{tag.name}}</div> + <div class="mr-1"><app-tag [tag]="tag" [clickable]="true" linkTitle="Filter by tag"></app-tag></div> <div class="badge bg-primary text-light rounded-pill ml-auto">{{tag.document_count}}</div> </button> </ng-container> From fa5121082de93cb0971304d887cdb05a4bce81be Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Wed, 9 Dec 2020 23:12:51 -0800 Subject: [PATCH 0029/1708] Moved quick filters to filter editor --- .../document-list.component.html | 85 +----------- .../document-list.component.scss | 10 -- .../document-list/document-list.component.ts | 82 +----------- .../filter-editor.component.html | 123 +++++++++++------- .../filter-editor.component.scss | 10 ++ .../filter-editor/filter-editor.component.ts | 69 +++++++++- 6 files changed, 154 insertions(+), 225 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index 96e13f935..13e1718ce 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -45,15 +45,8 @@ <div class="btn-group ml-2"> - <button type="button" class="btn btn-sm" [ngClass]="isFiltered ? 'btn-primary' : 'btn-outline-primary'" (click)="showFilter=!showFilter"> - <svg class="toolbaricon" fill="currentColor"> - <use xlink:href="assets/bootstrap-icons.svg#funnel" /> - </svg> - {{ showFilter ? 'Hide' : 'Show' }} Filter Editor - </button> - <div class="btn-group" ngbDropdown role="group"> - <button class="btn btn-sm btn-outline-primary dropdown-toggle-split" ngbDropdownToggle></button> + <button class="btn btn-sm btn-outline-primary dropdown-toggle" ngbDropdownToggle>Saved Views</button> <div class="dropdown-menu" ngbDropdownMenu> <ng-container *ngIf="!list.savedViewId"> <button ngbDropdownItem *ngFor="let config of savedViewConfigService.getConfigs()" (click)="loadViewConfig(config)">{{config.title}}</button> @@ -69,82 +62,8 @@ </app-page-header> -<div class="row pb-1 mb-3 align-items-right"> - <div class="btn-toolbar col-auto"> - <span class="text-muted mt-1 mr-2">Quick Filters:</span> - <div class="btn-group ml-2" ngbDropdown role="group"> - <button class="btn btn-outline-primary btn-sm" id="dropdownTags" ngbDropdownToggle>Tags</button> - <div class="dropdown-menu quick-filter" ngbDropdownMenu aria-labelledby="dropdownTags"> - <div class="list-group list-group-flush"> - <input class="list-group-item form-control" type="text" [(ngModel)]="filterTagsText" placeholder="Filter tags"> - <ng-container *ngIf="(tags | filter: filterTagsText).length > 0"> - <button class="list-group-item list-group-item-action d-flex align-items-center" role="menuitem" *ngFor="let tag of tags | filter: filterTagsText; let i = index" (click)="toggleFilterByTag(tag.id)"> - <div class="selected-icon mr-1"> - <svg *ngIf="currentViewIncludesTag(tag.id)" 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 [tag]="tag" [clickable]="true" linkTitle="Filter by tag"></app-tag></div> - <div class="badge bg-primary text-light rounded-pill ml-auto">{{tag.document_count}}</div> - </button> - </ng-container> - </div> - </div> - </div> - - <div class="btn-group ml-2" ngbDropdown role="group"> - <button class="btn btn-outline-primary btn-sm" id="dropdownCorrespondents" ngbDropdownToggle>Correspondents</button> - <div class="dropdown-menu quick-filter" ngbDropdownMenu aria-labelledby="dropdownCorrespondents"> - <div class="list-group list-group-flush"> - <input class="list-group-item form-control" type="text" [(ngModel)]="filterCorrespondentsText" placeholder="Filter correspondents"> - <ng-container *ngIf="(correspondents | filter: filterCorrespondentsText).length > 0"> - <button class="list-group-item list-group-item-action d-flex align-items-center" role="menuitem" *ngFor="let correspondent of correspondents | filter: filterCorrespondentsText; let i = index" (click)="toggleFilterByCorrespondent(correspondent.id)"> - <div class="selected-icon mr-1"> - <svg *ngIf="currentViewIncludesCorrespondent(correspondent.id)" 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">{{correspondent.name}}</div> - <div class="badge bg-primary text-light rounded-pill ml-auto">{{correspondent.document_count}}</div> - </button> - </ng-container> - </div> - </div> - </div> - - <div class="btn-group ml-2" ngbDropdown role="group"> - <button class="btn btn-outline-primary btn-sm" id="dropdownDocumentTypes" ngbDropdownToggle>Document Types</button> - <div class="dropdown-menu quick-filter" ngbDropdownMenu aria-labelledby="dropdownDocumentTypes"> - <div class="list-group list-group-flush"> - <input class="list-group-item form-control" type="text" [(ngModel)]="filterDocumentTypesText" placeholder="Filter document types"> - <ng-container *ngIf="(documentTypes | filter: filterDocumentTypesText).length > 0"> - <button class="list-group-item list-group-item-action d-flex align-items-center" role="menuitem" *ngFor="let documentType of documentTypes | filter: filterDocumentTypesText; let i = index" (click)="toggleFilterByDocumentType(documentType.id)"> - <div class="selected-icon mr-1"> - <svg *ngIf="currentViewIncludesDocumentType(documentType.id)" 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">{{documentType.name}}</div> - <div class="badge bg-primary text-light rounded-pill ml-auto">{{documentType.document_count}}</div> - </button> - </ng-container> - </div> - </div> - </div> - - <button class="btn-link border-0 bg-transparent ml-3 text-muted" *ngIf="currentViewIncludesQuickFilter()" (click)="filterEditor.clearClicked()"> - <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 - </button> - - </div> -</div> - -<div class="card w-100 mb-3" [hidden]="!showFilter"> +<div class="card w-100 mb-3"> <div class="card-body"> - <h5 class="card-title">Filter Editor</h5> <app-filter-editor [(filterRules)]="filterRules" (apply)="applyFilterRules()" (clear)="clearFilterRules()"></app-filter-editor> </div> </div> diff --git a/src-ui/src/app/components/document-list/document-list.component.scss b/src-ui/src/app/components/document-list/document-list.component.scss index 2513a1adc..e69de29bb 100644 --- a/src-ui/src/app/components/document-list/document-list.component.scss +++ b/src-ui/src/app/components/document-list/document-list.component.scss @@ -1,10 +0,0 @@ -.quick-filter { - min-width: 250px; - max-height: 400px; - overflow-y: scroll; - - .selected-icon { - min-width: 1em; - min-height: 1em; - } -} diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index 91ccfb082..d6b7c1d29 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, ViewChild } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; @@ -14,9 +14,6 @@ import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-vi 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 { FilterEditorComponent } from 'src/app/components/filter-editor/filter-editor.component'; @Component({ @@ -32,25 +29,12 @@ export class DocumentListComponent implements OnInit { public route: ActivatedRoute, private toastService: ToastService, public modalService: NgbModal, - private titleService: Title, - private tagService: TagService, - private correspondentService: CorrespondentService, - private documentTypeService: DocumentTypeService) { } + private titleService: Title) { } displayMode = 'smallCards' // largeCards, smallCards, details filterRules: FilterRule[] = [] - showFilter = false - tags: PaperlessTag[] = [] - correspondents: PaperlessCorrespondent[] = [] - documentTypes: PaperlessDocumentType[] = [] - filterTagsText: string - filterCorrespondentsText: string - filterDocumentTypesText: string - - @ViewChild(FilterEditorComponent) filterEditor; - get isFiltered() { return this.list.filterRules?.length > 0 } @@ -75,20 +59,15 @@ export class DocumentListComponent implements OnInit { if (params.has('id')) { this.list.savedView = this.savedViewConfigService.getConfig(params.get('id')) this.filterRules = this.list.filterRules - this.showFilter = false this.titleService.setTitle(`${this.list.savedView.title} - ${environment.appTitle}`) } else { this.list.savedView = null this.filterRules = this.list.filterRules - this.showFilter = this.filterRules.length > 0 this.titleService.setTitle(`Documents - ${environment.appTitle}`) } this.list.clear() this.list.reload() }) - 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) } applyFilterRules() { @@ -97,7 +76,6 @@ export class DocumentListComponent implements OnInit { clearFilterRules() { this.list.filterRules = this.filterRules - this.showFilter = false } loadViewConfig(config: SavedViewConfig) { @@ -164,60 +142,4 @@ export class DocumentListComponent implements OnInit { this.applyFilterRules() } - findRuleIndex(type_id: number, value: any) { - return this.list.filterRules.findIndex(rule => rule.type.id == type_id && rule.value == value) - } - - toggleFilterByTag(tag_id: number) { - let existingRuleIndex = this.findRuleIndex(FILTER_HAS_TAG, tag_id) - if (existingRuleIndex !== -1) { - let filterRules = this.list.filterRules - filterRules.splice(existingRuleIndex, 1) - this.filterRules = filterRules - this.applyFilterRules() - } else { - this.filterByTag(tag_id) - } - } - - toggleFilterByCorrespondent(correspondent_id: number) { - let existingRuleIndex = this.findRuleIndex(FILTER_CORRESPONDENT, correspondent_id) - if (existingRuleIndex !== -1) { - let filterRules = this.list.filterRules - filterRules.splice(existingRuleIndex, 1) - this.filterRules = filterRules - this.applyFilterRules() - } else { - this.filterByCorrespondent(correspondent_id) - } - } - - toggleFilterByDocumentType(document_type_id: number) { - let existingRuleIndex = this.findRuleIndex(FILTER_DOCUMENT_TYPE, document_type_id) - if (existingRuleIndex !== -1) { - let filterRules = this.list.filterRules - filterRules.splice(existingRuleIndex, 1) - this.filterRules = filterRules - this.applyFilterRules() - } else { - this.filterByDocumentType(document_type_id) - } - } - - currentViewIncludesTag(tag_id: number) { - return this.findRuleIndex(FILTER_HAS_TAG, tag_id) !== -1 - } - - currentViewIncludesCorrespondent(correspondent_id: number) { - return this.findRuleIndex(FILTER_CORRESPONDENT, correspondent_id) !== -1 - } - - currentViewIncludesDocumentType(document_type_id: number) { - return this.findRuleIndex(FILTER_DOCUMENT_TYPE, document_type_id) !== -1 - } - - currentViewIncludesQuickFilter() { - return this.list.filterRules.find(rule => rule.type.id == FILTER_HAS_TAG || rule.type.id == FILTER_CORRESPONDENT || rule.type.id == FILTER_DOCUMENT_TYPE) !== undefined - } - } diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.html b/src-ui/src/app/components/filter-editor/filter-editor.component.html index 48780e950..925b216bd 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.html +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.html @@ -1,52 +1,83 @@ -<div *ngFor="let rule of filterRules" class="form-row form-group"> - <div class="col-md-3 col-form-label"> - <span>{{rule.type.name}}</span> +<div class="form-row form-group mb-0"> + <div class="col-auto"> + <div class="text-muted mt-1">Filter by:</div> </div> <div class="col"> - <input *ngIf="rule.type.datatype == 'string'" type="text" class="form-control form-control-sm" [(ngModel)]="rule.value"> - <input *ngIf="rule.type.datatype == 'number'" type="number" class="form-control form-control-sm" [(ngModel)]="rule.value"> - <input *ngIf="rule.type.datatype == 'date'" type="date" class="form-control form-control-sm" [(ngModel)]="rule.value"> - - <select *ngIf="rule.type.datatype == 'tag'" class="form-control form-control-sm" [(ngModel)]="rule.value"> - <option *ngFor="let t of tags" [ngValue]="t.id">{{t.name}}</option> - </select> - - <select *ngIf="rule.type.datatype == 'document_type'" class="form-control form-control-sm" [(ngModel)]="rule.value"> - <option *ngFor="let dt of documentTypes" [ngValue]="dt.id">{{dt.name}}</option> - </select> - - <select *ngIf="rule.type.datatype == 'correspondent'" class="form-control form-control-sm" [(ngModel)]="rule.value"> - <option *ngFor="let c of correspondents" [ngValue]="c.id">{{c.name}}</option> - </select> - - <select *ngIf="rule.type.datatype == 'boolean'" class="form-control form-control-sm" [(ngModel)]="rule.value"> - <option [ngValue]="true">Yes</option> - <option [ngValue]="false">No</option> - </select> - + <input class="form-control form-control-sm" type="text" placeholder="Title / content"> </div> - <div class="col-auto"> - <button class="btn btn-sm btn-outline-secondary" (click)="removeRuleClicked(rule)"> - <svg class="toolbaricon" fill="currentColor"> - <use xlink:href="assets/bootstrap-icons.svg#x"/> - </svg> - </button> + + <div class="btn-group col-auto" ngbDropdown role="group"> + <button class="btn btn-outline-primary btn-sm" id="dropdownTags" ngbDropdownToggle>Tags</button> + <div class="dropdown-menu quick-filter" ngbDropdownMenu aria-labelledby="dropdownTags"> + <div class="list-group list-group-flush"> + <input class="list-group-item form-control" type="text" [(ngModel)]="filterTagsText" placeholder="Filter tags"> + <ng-container *ngIf="(tags | filter: filterTagsText).length > 0"> + <button class="list-group-item list-group-item-action d-flex align-items-center" role="menuitem" *ngFor="let tag of tags | filter: filterTagsText; let i = index" (click)="toggleFilterByTag(tag.id)"> + <div class="selected-icon mr-1"> + <svg *ngIf="currentViewIncludesTag(tag.id)" 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 [tag]="tag" [clickable]="true" linkTitle="Filter by tag"></app-tag></div> + <div class="badge bg-primary text-light rounded-pill ml-auto">{{tag.document_count}}</div> + </button> + </ng-container> + </div> + </div> + </div> + + <div class="btn-group col-auto" ngbDropdown role="group"> + <button class="btn btn-outline-primary btn-sm" id="dropdownCorrespondents" ngbDropdownToggle>Correspondents</button> + <div class="dropdown-menu quick-filter" ngbDropdownMenu aria-labelledby="dropdownCorrespondents"> + <div class="list-group list-group-flush"> + <input class="list-group-item form-control" type="text" [(ngModel)]="filterCorrespondentsText" placeholder="Filter correspondents"> + <ng-container *ngIf="(correspondents | filter: filterCorrespondentsText).length > 0"> + <button class="list-group-item list-group-item-action d-flex align-items-center" role="menuitem" *ngFor="let correspondent of correspondents | filter: filterCorrespondentsText; let i = index" (click)="toggleFilterByCorrespondent(correspondent.id)"> + <div class="selected-icon mr-1"> + <svg *ngIf="currentViewIncludesCorrespondent(correspondent.id)" 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">{{correspondent.name}}</div> + <div class="badge bg-primary text-light rounded-pill ml-auto">{{correspondent.document_count}}</div> + </button> + </ng-container> + </div> + </div> + </div> + + <div class="btn-group col-auto" ngbDropdown role="group"> + <button class="btn btn-outline-primary btn-sm" id="dropdownDocumentTypes" ngbDropdownToggle>Document Types</button> + <div class="dropdown-menu quick-filter" ngbDropdownMenu aria-labelledby="dropdownDocumentTypes"> + <div class="list-group list-group-flush"> + <input class="list-group-item form-control" type="text" [(ngModel)]="filterDocumentTypesText" placeholder="Filter document types"> + <ng-container *ngIf="(documentTypes | filter: filterDocumentTypesText).length > 0"> + <button class="list-group-item list-group-item-action d-flex align-items-center" role="menuitem" *ngFor="let documentType of documentTypes | filter: filterDocumentTypesText; let i = index" (click)="toggleFilterByDocumentType(documentType.id)"> + <div class="selected-icon mr-1"> + <svg *ngIf="currentViewIncludesDocumentType(documentType.id)" 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">{{documentType.name}}</div> + <div class="badge bg-primary text-light rounded-pill ml-auto">{{documentType.document_count}}</div> + </button> + </ng-container> + </div> + </div> + </div> + + <div class="btn-group col-auto" ngbDropdown role="group"> + <button class="btn btn-outline-primary btn-sm" id="dropdownCreated" ngbDropdownToggle>Created</button> + </div> + + <div class="btn-group col-auto" ngbDropdown role="group"> + <button class="btn btn-outline-primary btn-sm" id="dropdownAdded" ngbDropdownToggle>Added</button> </div> </div> -<div class="form-row form-group"> - <div class="col"> - <select [(ngModel)]="selectedRuleType" class="form-control form-control-sm"> - <option *ngFor="let ruleType of getRuleTypes()" [ngValue]="ruleType">{{ruleType.name}}</option> - </select> - </div> - <div class="col-auto"> - <button (click)="newRuleClicked()" class="btn btn-sm btn-outline-secondary">Add</button> - </div> - <div class="col-auto"> - <button (click)="clearClicked()" class="btn btn-sm btn-outline-secondary">Clear</button> - </div> - <div class="col-auto"> - <button (click)="applyClicked()" class="btn btn-sm btn-outline-secondary">Apply</button> - </div> -</div> +<button class="btn-link border-0 bg-transparent ml-3 text-muted" *ngIf="hasFilters()" (click)="clearClicked()"> + <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 +</button> diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.scss b/src-ui/src/app/components/filter-editor/filter-editor.component.scss index e69de29bb..05df7b213 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.scss +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.scss @@ -0,0 +1,10 @@ +.quick-filter { + min-width: 250px; + max-height: 400px; + overflow-y: scroll; + + .selected-icon { + min-width: 1em; + min-height: 1em; + } +} diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/filter-editor/filter-editor.component.ts index b04127287..ac0133f16 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.ts @@ -1,6 +1,6 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { FilterRule } from 'src/app/data/filter-rule'; -import { FilterRuleType, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; +import { FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'; import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'; import { PaperlessTag } from 'src/app/data/paperless-tag'; @@ -27,15 +27,16 @@ export class FilterEditorComponent implements OnInit { @Output() apply = new EventEmitter() - selectedRuleType: FilterRuleType = FILTER_RULE_TYPES[0] - correspondents: PaperlessCorrespondent[] = [] tags: PaperlessTag[] = [] documentTypes: PaperlessDocumentType[] = [] + filterTagsText: string + filterCorrespondentsText: string + filterDocumentTypesText: string + newRuleClicked() { this.filterRules.push({type: this.selectedRuleType, value: this.selectedRuleType.default}) - this.selectedRuleType = this.getRuleTypes().length > 0 ? this.getRuleTypes()[0] : null } removeRuleClicked(rule) { @@ -54,14 +55,70 @@ export class FilterEditorComponent implements OnInit { this.clear.next() } + hasFilters() { + return this.filterRules.length > 0 + } + ngOnInit(): void { this.correspondentService.listAll().subscribe(result => {this.correspondents = result.results}) this.tagService.listAll().subscribe(result => this.tags = result.results) this.documentTypeService.listAll().subscribe(result => this.documentTypes = result.results) } - getRuleTypes() { - return FILTER_RULE_TYPES.filter(rt => rt.multi || !this.filterRules.find(r => r.type == rt)) + findRuleIndex(type_id: number, value: any) { + return this.filterRules.findIndex(rule => rule.type.id == type_id && rule.value == value) + } + + toggleFilterByTag(tag_id: number) { + let existingRuleIndex = this.findRuleIndex(FILTER_HAS_TAG, tag_id) + if (existingRuleIndex !== -1) { + let filterRules = this.filterRules + filterRules.splice(existingRuleIndex, 1) + this.filterRules = filterRules + this.applyFilterRules() + } else { + this.filterByTag(tag_id) + } + } + + toggleFilterByCorrespondent(correspondent_id: number) { + let existingRuleIndex = this.findRuleIndex(FILTER_CORRESPONDENT, correspondent_id) + if (existingRuleIndex !== -1) { + let filterRules = this.filterRules + filterRules.splice(existingRuleIndex, 1) + this.filterRules = filterRules + this.applyFilterRules() + } else { + this.filterByCorrespondent(correspondent_id) + } + } + + toggleFilterByDocumentType(document_type_id: number) { + let existingRuleIndex = this.findRuleIndex(FILTER_DOCUMENT_TYPE, document_type_id) + if (existingRuleIndex !== -1) { + let filterRules = this.filterRules + filterRules.splice(existingRuleIndex, 1) + this.filterRules = filterRules + this.applyFilterRules() + } else { + this.filterByDocumentType(document_type_id) + } + } + + currentViewIncludesTag(tag_id: number) { + return this.findRuleIndex(FILTER_HAS_TAG, tag_id) !== -1 + } + + currentViewIncludesCorrespondent(correspondent_id: number) { + return this.findRuleIndex(FILTER_CORRESPONDENT, correspondent_id) !== -1 + } + + currentViewIncludesDocumentType(document_type_id: number) { + return this.findRuleIndex(FILTER_DOCUMENT_TYPE, document_type_id) !== -1 + } + + currentViewIncludesQuickFilter() { + return this.filterRules.find(rule => rule.type.id == FILTER_HAS_TAG || rule.type.id == FILTER_CORRESPONDENT || rule.type.id == FILTER_DOCUMENT_TYPE) !== undefined } } From ab8a1cfded4c442965991f90c1639368b9dd70e0 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Thu, 10 Dec 2020 00:46:13 -0800 Subject: [PATCH 0030/1708] Working moved dropdowns --- .../document-list/document-list.component.ts | 45 +-------------- .../filter-editor.component.html | 14 ++--- .../filter-editor/filter-editor.component.ts | 55 ++++++++----------- 3 files changed, 33 insertions(+), 81 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index d6b7c1d29..3fb933fb7 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -11,10 +11,6 @@ import { SavedViewConfigService } from 'src/app/services/saved-view-config.servi import { Toast, ToastService } from 'src/app/services/toast.service'; import { environment } from 'src/environments/environment'; import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component'; -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 { FilterEditorComponent } from 'src/app/components/filter-editor/filter-editor.component'; @Component({ selector: 'app-document-list', @@ -71,6 +67,8 @@ export class DocumentListComponent implements OnInit { } applyFilterRules() { + console.log('applyFilterRules'); + this.list.filterRules = this.filterRules } @@ -103,43 +101,4 @@ export class DocumentListComponent implements OnInit { }) } - filterByTag(tag_id: number) { - let filterRules = this.list.filterRules - if (filterRules.find(rule => rule.type.id == FILTER_HAS_TAG && rule.value == tag_id)) { - return - } - - filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_HAS_TAG), value: tag_id}) - this.filterRules = filterRules - this.applyFilterRules() - } - - filterByCorrespondent(correspondent_id: number) { - let filterRules = this.list.filterRules - let existing_rule = filterRules.find(rule => rule.type.id == FILTER_CORRESPONDENT) - if (existing_rule && existing_rule.value == correspondent_id) { - return - } else if (existing_rule) { - existing_rule.value = correspondent_id - } else { - filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_CORRESPONDENT), value: correspondent_id}) - } - this.filterRules = filterRules - this.applyFilterRules() - } - - filterByDocumentType(document_type_id: number) { - let filterRules = this.list.filterRules - let existing_rule = filterRules.find(rule => rule.type.id == FILTER_DOCUMENT_TYPE) - if (existing_rule && existing_rule.value == document_type_id) { - return - } else if (existing_rule) { - existing_rule.value = document_type_id - } else { - filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_DOCUMENT_TYPE), value: document_type_id}) - } - this.filterRules = filterRules - this.applyFilterRules() - } - } diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.html b/src-ui/src/app/components/filter-editor/filter-editor.component.html index 925b216bd..756110d13 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.html +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.html @@ -73,11 +73,11 @@ <div class="btn-group col-auto" ngbDropdown role="group"> <button class="btn btn-outline-primary btn-sm" id="dropdownAdded" ngbDropdownToggle>Added</button> </div> -</div> -<button class="btn-link border-0 bg-transparent ml-3 text-muted" *ngIf="hasFilters()" (click)="clearClicked()"> - <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 -</button> + <button class="btn btn-outline-secondary btn-sm" [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> diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/filter-editor/filter-editor.component.ts index ac0133f16..f4dc7162b 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.ts @@ -35,22 +35,11 @@ export class FilterEditorComponent implements OnInit { filterCorrespondentsText: string filterDocumentTypesText: string - newRuleClicked() { - this.filterRules.push({type: this.selectedRuleType, value: this.selectedRuleType.default}) - } - - removeRuleClicked(rule) { - let index = this.filterRules.findIndex(r => r == rule) - if (index > -1) { - this.filterRules.splice(index, 1) - } - } - - applyClicked() { + applySelected() { this.apply.next() } - clearClicked() { + clearSelected() { this.filterRules.splice(0,this.filterRules.length) this.clear.next() } @@ -71,38 +60,42 @@ export class FilterEditorComponent implements OnInit { toggleFilterByTag(tag_id: number) { let existingRuleIndex = this.findRuleIndex(FILTER_HAS_TAG, tag_id) + let filterRules = this.filterRules if (existingRuleIndex !== -1) { - let filterRules = this.filterRules filterRules.splice(existingRuleIndex, 1) - this.filterRules = filterRules - this.applyFilterRules() } else { - this.filterByTag(tag_id) + filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_HAS_TAG), value: tag_id}) } + this.filterRules = filterRules + this.applySelected() } toggleFilterByCorrespondent(correspondent_id: number) { - let existingRuleIndex = this.findRuleIndex(FILTER_CORRESPONDENT, correspondent_id) - if (existingRuleIndex !== -1) { - let filterRules = this.filterRules - filterRules.splice(existingRuleIndex, 1) - this.filterRules = filterRules - this.applyFilterRules() + let filterRules = this.filterRules + let existingRule = filterRules.find(rule => rule.type.id == FILTER_CORRESPONDENT) + if (existingRule && existingRule.value == correspondent_id) { + return + } else if (existingRule) { + existingRule.value = correspondent_id } else { - this.filterByCorrespondent(correspondent_id) + filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_CORRESPONDENT), value: correspondent_id}) } + this.filterRules = filterRules + this.applySelected() } toggleFilterByDocumentType(document_type_id: number) { - let existingRuleIndex = this.findRuleIndex(FILTER_DOCUMENT_TYPE, document_type_id) - if (existingRuleIndex !== -1) { - let filterRules = this.filterRules - filterRules.splice(existingRuleIndex, 1) - this.filterRules = filterRules - this.applyFilterRules() + let filterRules = this.filterRules + let existingRule = filterRules.find(rule => rule.type.id == FILTER_DOCUMENT_TYPE) + if (existingRule && existingRule.value == document_type_id) { + return + } else if (existingRule) { + existingRule.value = document_type_id } else { - this.filterByDocumentType(document_type_id) + filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_DOCUMENT_TYPE), value: document_type_id}) } + this.filterRules = filterRules + this.applySelected() } currentViewIncludesTag(tag_id: number) { From 25e1177198eb6d62c3af0b0b2d7edea16c4432b5 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Thu, 10 Dec 2020 01:42:40 -0800 Subject: [PATCH 0031/1708] Title filtering --- .../document-list/document-list.component.ts | 2 - .../filter-editor.component.html | 2 +- .../filter-editor/filter-editor.component.ts | 48 +++++++++++++++++-- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index 3fb933fb7..3ce00beab 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -67,8 +67,6 @@ export class DocumentListComponent implements OnInit { } applyFilterRules() { - console.log('applyFilterRules'); - this.list.filterRules = this.filterRules } diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.html b/src-ui/src/app/components/filter-editor/filter-editor.component.html index 756110d13..586e23a98 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.html +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.html @@ -3,7 +3,7 @@ <div class="text-muted mt-1">Filter by:</div> </div> <div class="col"> - <input class="form-control form-control-sm" type="text" placeholder="Title / content"> + <input class="form-control form-control-sm" type="text" [(ngModel)]="filterText" placeholder="Title" #filterTextInput> </div> <div class="btn-group col-auto" ngbDropdown role="group"> diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/filter-editor/filter-editor.component.ts index f4dc7162b..e470a8400 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.ts @@ -1,20 +1,21 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnInit, Output, ElementRef, AfterViewInit, ViewChild } from '@angular/core'; import { FilterRule } from 'src/app/data/filter-rule'; -import { FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; +import { FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_TITLE, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'; import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'; import { PaperlessTag } from 'src/app/data/paperless-tag'; import { CorrespondentService } from 'src/app/services/rest/correspondent.service'; import { DocumentTypeService } from 'src/app/services/rest/document-type.service'; import { TagService } from 'src/app/services/rest/tag.service'; - +import { fromEvent } from 'rxjs'; +import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; @Component({ selector: 'app-filter-editor', templateUrl: './filter-editor.component.html', styleUrls: ['./filter-editor.component.scss'] }) -export class FilterEditorComponent implements OnInit { +export class FilterEditorComponent implements OnInit, AfterViewInit { constructor(private documentTypeService: DocumentTypeService, private tagService: TagService, private correspondentService: CorrespondentService) { } @@ -27,10 +28,13 @@ export class FilterEditorComponent implements OnInit { @Output() apply = new EventEmitter() + @ViewChild('filterTextInput') input: ElementRef; + correspondents: PaperlessCorrespondent[] = [] tags: PaperlessTag[] = [] documentTypes: PaperlessDocumentType[] = [] + filterText: string filterTagsText: string filterCorrespondentsText: string filterDocumentTypesText: string @@ -41,6 +45,7 @@ export class FilterEditorComponent implements OnInit { clearSelected() { this.filterRules.splice(0,this.filterRules.length) + this.updateTextFilterInput() this.clear.next() } @@ -52,12 +57,47 @@ export class FilterEditorComponent implements OnInit { this.correspondentService.listAll().subscribe(result => {this.correspondents = result.results}) this.tagService.listAll().subscribe(result => this.tags = result.results) this.documentTypeService.listAll().subscribe(result => this.documentTypes = result.results) + this.updateTextFilterInput() + } + + ngAfterViewInit() { + fromEvent(this.input.nativeElement,'keyup') + .pipe( + debounceTime(150), + distinctUntilChanged(), + tap() + ) + .subscribe(event => { + this.filterText = event.target.value + this.onTextFilterInput() + }); } findRuleIndex(type_id: number, value: any) { return this.filterRules.findIndex(rule => rule.type.id == type_id && rule.value == value) } + updateTextFilterInput() { + let existingTextRule = this.filterRules.find(rule => rule.type.id == FILTER_TITLE) + if (existingTextRule) this.filterText = existingTextRule.value + else this.filterText = '' + } + + onTextFilterInput(event) { + let text = this.filterText + let filterRules = this.filterRules + let existingRule = filterRules.find(rule => rule.type.id == FILTER_TITLE) + if (existingRule && existingRule.value == text) { + return + } else if (existingRule) { + existingRule.value = text + } else { + filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_TITLE), value: text}) + } + this.filterRules = filterRules + this.applySelected() + } + toggleFilterByTag(tag_id: number) { let existingRuleIndex = this.findRuleIndex(FILTER_HAS_TAG, tag_id) let filterRules = this.filterRules From 3a82b7806ac229b7c943de4b6aa02ec023ad863b Mon Sep 17 00:00:00 2001 From: Jonas Winkler <dev@jpwinkler.de> Date: Thu, 10 Dec 2020 16:28:02 +0100 Subject: [PATCH 0032/1708] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e754669a8..41f85af19 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Here's what you get: # Features * Performs OCR on your documents, adds selectable text to image only documents and adds tags, correspondents and document types to your documents. +* Paperless stores your documents plain on disk. Filenames and folders are managed by paperless and can be configured freely. * Single page application front end. Should be pretty snappy. Will be mobile friendly in the future. * Includes a dashboard that shows basic statistics and has document upload. * Filtering by tags, correspondents, types, and more. From edcb62476bdc2683066089c35e35290be8e6c9cb Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Thu, 10 Dec 2020 09:13:26 -0800 Subject: [PATCH 0033/1708] Remove clear button outline --- .../app/components/filter-editor/filter-editor.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.html b/src-ui/src/app/components/filter-editor/filter-editor.component.html index 586e23a98..5e2077116 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.html +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.html @@ -74,7 +74,7 @@ <button class="btn btn-outline-primary btn-sm" id="dropdownAdded" ngbDropdownToggle>Added</button> </div> - <button class="btn btn-outline-secondary btn-sm" [disabled]="!hasFilters()" (click)="clearSelected()"> + <button class="btn btn-link btn-sm" [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> From 9a189546869a5b877ed845decf7cce5771674f5e Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Thu, 10 Dec 2020 09:24:07 -0800 Subject: [PATCH 0034/1708] Fix unused method event parameter --- .../app/components/filter-editor/filter-editor.component.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/filter-editor/filter-editor.component.ts index e470a8400..ac89d9b88 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.ts @@ -67,8 +67,8 @@ export class FilterEditorComponent implements OnInit, AfterViewInit { distinctUntilChanged(), tap() ) - .subscribe(event => { - this.filterText = event.target.value + .subscribe((event: Event) => { + this.filterText = (event.target as HTMLInputElement).value this.onTextFilterInput() }); } @@ -83,7 +83,7 @@ export class FilterEditorComponent implements OnInit, AfterViewInit { else this.filterText = '' } - onTextFilterInput(event) { + onTextFilterInput() { let text = this.filterText let filterRules = this.filterRules let existingRule = filterRules.find(rule => rule.type.id == FILTER_TITLE) From db02d68a5a89272e4d30caf8e74065991a9cca48 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Thu, 10 Dec 2020 14:41:37 -0800 Subject: [PATCH 0035/1708] Refactored dropdowns to separate component --- src-ui/src/app/app.module.ts | 2 + .../filter-dropdown.component.html | 19 +++ .../filter-dropdown.component.scss | 10 ++ .../filter-dropdown.component.spec.ts | 25 ++++ .../filter-dropdown.component.ts | 34 ++++++ .../filter-editor.component.html | 63 +--------- .../filter-editor/filter-editor.component.ts | 109 +++++++----------- src-ui/src/app/data/filter-rule-type.ts | 20 ++-- 8 files changed, 146 insertions(+), 136 deletions(-) create mode 100644 src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html create mode 100644 src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.scss create mode 100644 src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.spec.ts create mode 100644 src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index af2c46492..6a847494a 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -28,6 +28,7 @@ import { PageHeaderComponent } from './components/common/page-header/page-header import { AppFrameComponent } from './components/app-frame/app-frame.component'; import { ToastsComponent } from './components/common/toasts/toasts.component'; import { FilterEditorComponent } from './components/filter-editor/filter-editor.component'; +import { FilterDropdownComponent } from './components/filter-editor/filter-dropdown/filter-dropdown.component'; import { 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 { NgxFileDropModule } from 'ngx-file-drop'; @@ -74,6 +75,7 @@ import { FilterPipe } from './pipes/filter.pipe'; AppFrameComponent, ToastsComponent, FilterEditorComponent, + FilterDropdownComponent, DocumentCardLargeComponent, DocumentCardSmallComponent, TextComponent, diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html new file mode 100644 index 000000000..b135caff0 --- /dev/null +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html @@ -0,0 +1,19 @@ + <div class="btn-group" ngbDropdown role="group"> + <button class="btn btn-outline-primary btn-sm" id="dropdown{{title}}" ngbDropdownToggle>{{title}}</button> + <div class="dropdown-menu quick-filter" ngbDropdownMenu aria-labelledby="dropdown{{title}}"> + <div class="list-group list-group-flush"> + <input class="list-group-item form-control" type="text" [(ngModel)]="filterText" placeholder="Filter {{title}}"> + <ng-container *ngIf="(items | filter: filterText).length > 0"> + <button class="list-group-item list-group-item-action d-flex align-items-center" role="menuitem" *ngFor="let item of items | filter: filterText; let i = index" (click)="toggleItem(item)"> + <div class="selected-icon mr-1"> + <svg *ngIf="itemsActive.includes(item)" 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">{{item.name}}</div> + <div class="badge bg-primary text-light rounded-pill ml-auto">{{item.document_count}}</div> + </button> + </ng-container> + </div> + </div> +</div> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.scss b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.scss new file mode 100644 index 000000000..05df7b213 --- /dev/null +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.scss @@ -0,0 +1,10 @@ +.quick-filter { + min-width: 250px; + max-height: 400px; + overflow-y: scroll; + + .selected-icon { + min-width: 1em; + min-height: 1em; + } +} diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.spec.ts b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.spec.ts new file mode 100644 index 000000000..29edd7c45 --- /dev/null +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FilterDropodownComponent } from './filter-dropdown.component'; + +describe('FilterDropodownComponent', () => { + let component: FilterDropodownComponent; + let fixture: ComponentFixture<FilterDropodownComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ FilterDropodownComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FilterDropodownComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts new file mode 100644 index 000000000..443fd30e4 --- /dev/null +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts @@ -0,0 +1,34 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FilterRuleType, FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_TITLE, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; +import { ObjectWithId } from 'src/app/data/object-with-id'; +import { MatchingModel } from 'src/app/data/matching-model'; + +@Component({ + selector: 'app-filter-dropdown', + templateUrl: './filter-dropdown.component.html', + styleUrls: ['./filter-dropdown.component.scss'] +}) +export class FilterDropdownComponent implements OnInit { + + constructor() { } + + @Input() + filterRuleTypeID: number + + @Output() + toggle = new EventEmitter() + + items: MatchingModel[] = [] + itemsActive: MatchingModel[] = [] + title: string + filterText: string + + ngOnInit(): void { + let filterRuleType: FilterRuleType = FILTER_RULE_TYPES.find(t => t.id == this.filterRuleTypeID) + this.title = filterRuleType.name + } + + toggleItem(item: ObjectWithId) { + this.toggle.emit(item, this.filterRuleTypeID) + } +} diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.html b/src-ui/src/app/components/filter-editor/filter-editor.component.html index 5e2077116..153f32644 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.html +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.html @@ -6,65 +6,10 @@ <input class="form-control form-control-sm" type="text" [(ngModel)]="filterText" placeholder="Title" #filterTextInput> </div> - <div class="btn-group col-auto" ngbDropdown role="group"> - <button class="btn btn-outline-primary btn-sm" id="dropdownTags" ngbDropdownToggle>Tags</button> - <div class="dropdown-menu quick-filter" ngbDropdownMenu aria-labelledby="dropdownTags"> - <div class="list-group list-group-flush"> - <input class="list-group-item form-control" type="text" [(ngModel)]="filterTagsText" placeholder="Filter tags"> - <ng-container *ngIf="(tags | filter: filterTagsText).length > 0"> - <button class="list-group-item list-group-item-action d-flex align-items-center" role="menuitem" *ngFor="let tag of tags | filter: filterTagsText; let i = index" (click)="toggleFilterByTag(tag.id)"> - <div class="selected-icon mr-1"> - <svg *ngIf="currentViewIncludesTag(tag.id)" 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 [tag]="tag" [clickable]="true" linkTitle="Filter by tag"></app-tag></div> - <div class="badge bg-primary text-light rounded-pill ml-auto">{{tag.document_count}}</div> - </button> - </ng-container> - </div> - </div> - </div> - - <div class="btn-group col-auto" ngbDropdown role="group"> - <button class="btn btn-outline-primary btn-sm" id="dropdownCorrespondents" ngbDropdownToggle>Correspondents</button> - <div class="dropdown-menu quick-filter" ngbDropdownMenu aria-labelledby="dropdownCorrespondents"> - <div class="list-group list-group-flush"> - <input class="list-group-item form-control" type="text" [(ngModel)]="filterCorrespondentsText" placeholder="Filter correspondents"> - <ng-container *ngIf="(correspondents | filter: filterCorrespondentsText).length > 0"> - <button class="list-group-item list-group-item-action d-flex align-items-center" role="menuitem" *ngFor="let correspondent of correspondents | filter: filterCorrespondentsText; let i = index" (click)="toggleFilterByCorrespondent(correspondent.id)"> - <div class="selected-icon mr-1"> - <svg *ngIf="currentViewIncludesCorrespondent(correspondent.id)" 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">{{correspondent.name}}</div> - <div class="badge bg-primary text-light rounded-pill ml-auto">{{correspondent.document_count}}</div> - </button> - </ng-container> - </div> - </div> - </div> - - <div class="btn-group col-auto" ngbDropdown role="group"> - <button class="btn btn-outline-primary btn-sm" id="dropdownDocumentTypes" ngbDropdownToggle>Document Types</button> - <div class="dropdown-menu quick-filter" ngbDropdownMenu aria-labelledby="dropdownDocumentTypes"> - <div class="list-group list-group-flush"> - <input class="list-group-item form-control" type="text" [(ngModel)]="filterDocumentTypesText" placeholder="Filter document types"> - <ng-container *ngIf="(documentTypes | filter: filterDocumentTypesText).length > 0"> - <button class="list-group-item list-group-item-action d-flex align-items-center" role="menuitem" *ngFor="let documentType of documentTypes | filter: filterDocumentTypesText; let i = index" (click)="toggleFilterByDocumentType(documentType.id)"> - <div class="selected-icon mr-1"> - <svg *ngIf="currentViewIncludesDocumentType(documentType.id)" 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">{{documentType.name}}</div> - <div class="badge bg-primary text-light rounded-pill ml-auto">{{documentType.document_count}}</div> - </button> - </ng-container> - </div> - </div> - </div> + <app-filter-dropdown class="col-auto" *ngFor="let quickFilterRuleTypeID of quickFilterRuleTypeIDs" + [filterRuleTypeID]="quickFilterRuleTypeID" + (toggle)="toggleFilterByItem($event, quickFilterRuleTypeID)"> + </app-filter-dropdown> <div class="btn-group col-auto" ngbDropdown role="group"> <button class="btn btn-outline-primary btn-sm" id="dropdownCreated" ngbDropdownToggle>Created</button> diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/filter-editor/filter-editor.component.ts index ac89d9b88..b79fbaf12 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.ts @@ -1,12 +1,15 @@ -import { Component, EventEmitter, Input, OnInit, Output, ElementRef, AfterViewInit, ViewChild } from '@angular/core'; +import { Component, EventEmitter, Input, OnInit, Output, ElementRef, AfterViewInit, QueryList, ViewChild, ViewChildren } from '@angular/core'; import { FilterRule } from 'src/app/data/filter-rule'; -import { FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_TITLE, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; +import { FilterRuleType, FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_TITLE, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'; import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'; import { PaperlessTag } from 'src/app/data/paperless-tag'; +import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'; +import { ObjectWithId } from 'src/app/data/object-with-id'; import { CorrespondentService } from 'src/app/services/rest/correspondent.service'; import { DocumentTypeService } from 'src/app/services/rest/document-type.service'; import { TagService } from 'src/app/services/rest/tag.service'; +import { FilterDropdownComponent } from './filter-dropdown/filter-dropdown.component' import { fromEvent } from 'rxjs'; import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; @@ -29,6 +32,9 @@ export class FilterEditorComponent implements OnInit, AfterViewInit { apply = new EventEmitter() @ViewChild('filterTextInput') input: ElementRef; + @ViewChildren(FilterDropdownComponent) quickFilterDropdowns!: QueryList<FilterDropdownComponent>; + + quickFilterRuleTypeIDs: number[] = [FILTER_HAS_TAG, FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE] correspondents: PaperlessCorrespondent[] = [] tags: PaperlessTag[] = [] @@ -39,25 +45,11 @@ export class FilterEditorComponent implements OnInit, AfterViewInit { filterCorrespondentsText: string filterDocumentTypesText: string - applySelected() { - this.apply.next() - } - - clearSelected() { - this.filterRules.splice(0,this.filterRules.length) - this.updateTextFilterInput() - this.clear.next() - } - - hasFilters() { - return this.filterRules.length > 0 - } - ngOnInit(): void { - this.correspondentService.listAll().subscribe(result => {this.correspondents = result.results}) - this.tagService.listAll().subscribe(result => this.tags = result.results) - this.documentTypeService.listAll().subscribe(result => this.documentTypes = result.results) this.updateTextFilterInput() + this.tagService.listAll().subscribe(result => this.setDropdownItems(result.results, FILTER_HAS_TAG)) + this.correspondentService.listAll().subscribe(result => this.setDropdownItems(result.results, FILTER_CORRESPONDENT)) + this.documentTypeService.listAll().subscribe(result => this.setDropdownItems(result.results, FILTER_DOCUMENT_TYPE)) } ngAfterViewInit() { @@ -73,8 +65,29 @@ export class FilterEditorComponent implements OnInit, AfterViewInit { }); } - findRuleIndex(type_id: number, value: any) { - return this.filterRules.findIndex(rule => rule.type.id == type_id && rule.value == value) + setDropdownItems(items: ObjectWithId[], filterRuleTypeID: number) { + let dropdown: FilterDropdownComponent = this.getDropdownByFilterRuleTypeID(filterRuleTypeID) + if (dropdown) { + dropdown.items = items + } + } + + getDropdownByFilterRuleTypeID(filterRuleTypeID: number): FilterDropdownComponent { + return this.quickFilterDropdowns.find(d => d.filterRuleTypeID == filterRuleTypeID) + } + + applySelected() { + this.apply.next() + } + + clearSelected() { + this.filterRules.splice(0,this.filterRules.length) + this.updateTextFilterInput() + this.clear.next() + } + + hasFilters() { + return this.filterRules.length > 0 } updateTextFilterInput() { @@ -98,60 +111,22 @@ export class FilterEditorComponent implements OnInit, AfterViewInit { this.applySelected() } - toggleFilterByTag(tag_id: number) { - let existingRuleIndex = this.findRuleIndex(FILTER_HAS_TAG, tag_id) + toggleFilterByItem(item: ObjectWithId, filterRuleTypeID: number) { let filterRules = this.filterRules - if (existingRuleIndex !== -1) { - filterRules.splice(existingRuleIndex, 1) - } else { - filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_HAS_TAG), value: tag_id}) - } - this.filterRules = filterRules - this.applySelected() - } + let filterRuleType: FilterRuleType = FILTER_RULE_TYPES.find(t => t.id == filterRuleTypeID) + let existingRule = filterRules.find(rule => rule.type.id == filterRuleType.id) - toggleFilterByCorrespondent(correspondent_id: number) { - let filterRules = this.filterRules - let existingRule = filterRules.find(rule => rule.type.id == FILTER_CORRESPONDENT) - if (existingRule && existingRule.value == correspondent_id) { + if (existingRule && existingRule.value == item.id && filterRuleType.id == FILTER_HAS_TAG) { + filterRules.splice(filterRules.indexOf(existingRule), 1) + } else if (existingRule && existingRule.value == item.id) { return } else if (existingRule) { - existingRule.value = correspondent_id + existingRule.value = item.id } else { - filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_CORRESPONDENT), value: correspondent_id}) + filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == filterRuleType.id), value: item.id}) } this.filterRules = filterRules this.applySelected() } - toggleFilterByDocumentType(document_type_id: number) { - let filterRules = this.filterRules - let existingRule = filterRules.find(rule => rule.type.id == FILTER_DOCUMENT_TYPE) - if (existingRule && existingRule.value == document_type_id) { - return - } else if (existingRule) { - existingRule.value = document_type_id - } else { - filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_DOCUMENT_TYPE), value: document_type_id}) - } - this.filterRules = filterRules - this.applySelected() - } - - currentViewIncludesTag(tag_id: number) { - return this.findRuleIndex(FILTER_HAS_TAG, tag_id) !== -1 - } - - currentViewIncludesCorrespondent(correspondent_id: number) { - return this.findRuleIndex(FILTER_CORRESPONDENT, correspondent_id) !== -1 - } - - currentViewIncludesDocumentType(document_type_id: number) { - return this.findRuleIndex(FILTER_DOCUMENT_TYPE, document_type_id) !== -1 - } - - currentViewIncludesQuickFilter() { - return this.filterRules.find(rule => rule.type.id == FILTER_HAS_TAG || rule.type.id == FILTER_CORRESPONDENT || rule.type.id == FILTER_DOCUMENT_TYPE) !== undefined - } - } diff --git a/src-ui/src/app/data/filter-rule-type.ts b/src-ui/src/app/data/filter-rule-type.ts index a35759f69..1a174ce57 100644 --- a/src-ui/src/app/data/filter-rule-type.ts +++ b/src-ui/src/app/data/filter-rule-type.ts @@ -22,15 +22,15 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [ {id: FILTER_TITLE, name: "Title contains", filtervar: "title__icontains", datatype: "string", multi: false, default: ""}, {id: FILTER_CONTENT, name: "Content contains", filtervar: "content__icontains", datatype: "string", multi: false, default: ""}, - - {id: FILTER_ASN, name: "ASN is", filtervar: "archive_serial_number", datatype: "number", multi: false}, - - {id: FILTER_CORRESPONDENT, name: "Correspondent is", filtervar: "correspondent__id", datatype: "correspondent", multi: false}, - {id: FILTER_DOCUMENT_TYPE, name: "Document type is", filtervar: "document_type__id", datatype: "document_type", multi: false}, - {id: FILTER_IS_IN_INBOX, name: "Is in Inbox", filtervar: "is_in_inbox", datatype: "boolean", multi: false, default: true}, - {id: FILTER_HAS_TAG, name: "Has tag", filtervar: "tags__id__all", datatype: "tag", multi: true}, - {id: FILTER_DOES_NOT_HAVE_TAG, name: "Does not have tag", filtervar: "tags__id__none", datatype: "tag", multi: true}, + {id: FILTER_ASN, name: "ASN is", filtervar: "archive_serial_number", datatype: "number", multi: false}, + + {id: FILTER_CORRESPONDENT, name: "Correspondents", filtervar: "correspondent__id", datatype: "correspondent", multi: false}, + {id: FILTER_DOCUMENT_TYPE, name: "Document types", filtervar: "document_type__id", datatype: "document_type", multi: false}, + + {id: FILTER_IS_IN_INBOX, name: "Is in Inbox", filtervar: "is_in_inbox", datatype: "boolean", multi: false, default: true}, + {id: FILTER_HAS_TAG, name: "Tags", filtervar: "tags__id__all", datatype: "tag", multi: true}, + {id: FILTER_DOES_NOT_HAVE_TAG, name: "Does not have tag", filtervar: "tags__id__none", datatype: "tag", multi: true}, {id: FILTER_HAS_ANY_TAG, name: "Has any tag", filtervar: "is_tagged", datatype: "boolean", multi: false, default: true}, {id: FILTER_CREATED_BEFORE, name: "Created before", filtervar: "created__date__lt", datatype: "date", multi: false}, @@ -42,7 +42,7 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [ {id: FILTER_ADDED_BEFORE, name: "Added before", filtervar: "added__date__lt", datatype: "date", multi: false}, {id: FILTER_ADDED_AFTER, name: "Added after", filtervar: "added__date__gt", datatype: "date", multi: false}, - + {id: FILTER_MODIFIED_BEFORE, name: "Modified before", filtervar: "modified__date__lt", datatype: "date", multi: false}, {id: FILTER_MODIFIED_AFTER, name: "Modified after", filtervar: "modified__date__gt", datatype: "date", multi: false}, ] @@ -54,4 +54,4 @@ export interface FilterRuleType { datatype: string //number, string, boolean, date multi: boolean default?: any -} \ No newline at end of file +} From f83185bfe46f6a2b118cb05d39cffae931d1518e Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Thu, 10 Dec 2020 15:36:17 -0800 Subject: [PATCH 0036/1708] Refactored dropdowns allow clearing, active checkmarks --- .../filter-dropdown.component.html | 2 +- .../filter-dropdown.component.ts | 7 +++---- .../filter-editor/filter-editor.component.ts | 19 ++++++++++++++++++- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html index b135caff0..4552406b1 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html @@ -1,6 +1,6 @@ <div class="btn-group" ngbDropdown role="group"> <button class="btn btn-outline-primary btn-sm" id="dropdown{{title}}" ngbDropdownToggle>{{title}}</button> - <div class="dropdown-menu quick-filter" ngbDropdownMenu aria-labelledby="dropdown{{title}}"> + <div class="dropdown-menu quick-filter" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> <div class="list-group list-group-flush"> <input class="list-group-item form-control" type="text" [(ngModel)]="filterText" placeholder="Filter {{title}}"> <ng-container *ngIf="(items | filter: filterText).length > 0"> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts index 443fd30e4..070689bd2 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts @@ -1,7 +1,6 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { FilterRuleType, FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_TITLE, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; import { ObjectWithId } from 'src/app/data/object-with-id'; -import { MatchingModel } from 'src/app/data/matching-model'; @Component({ selector: 'app-filter-dropdown', @@ -18,8 +17,8 @@ export class FilterDropdownComponent implements OnInit { @Output() toggle = new EventEmitter() - items: MatchingModel[] = [] - itemsActive: MatchingModel[] = [] + items: ObjectWithId[] = [] + itemsActive: ObjectWithId[] = [] title: string filterText: string @@ -29,6 +28,6 @@ export class FilterDropdownComponent implements OnInit { } toggleItem(item: ObjectWithId) { - this.toggle.emit(item, this.filterRuleTypeID) + this.toggle.emit(item) } } diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/filter-editor/filter-editor.component.ts index b79fbaf12..d45732717 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.ts @@ -65,11 +65,21 @@ export class FilterEditorComponent implements OnInit, AfterViewInit { }); } - setDropdownItems(items: ObjectWithId[], filterRuleTypeID: number) { + setDropdownItems(items: ObjectWithId[], filterRuleTypeID: number): void { let dropdown: FilterDropdownComponent = this.getDropdownByFilterRuleTypeID(filterRuleTypeID) if (dropdown) { dropdown.items = items } + this.updateDropdownActiveItems(dropdown) + } + + updateDropdownActiveItems(dropdown: FilterDropdownComponent): void { + let activeRulesValues = this.filterRules.filter(r => r.type.id == dropdown.filterRuleTypeID).map(r => r.value) + let activeItems = [] + if (activeRulesValues.length > 0) { + activeItems = dropdown.items.filter(i => activeRulesValues.includes(i.id)) + } + dropdown.itemsActive = activeItems } getDropdownByFilterRuleTypeID(filterRuleTypeID: number): FilterDropdownComponent { @@ -83,6 +93,7 @@ export class FilterEditorComponent implements OnInit, AfterViewInit { clearSelected() { this.filterRules.splice(0,this.filterRules.length) this.updateTextFilterInput() + this.quickFilterDropdowns.forEach(d => this.updateDropdownActiveItems(d)) this.clear.next() } @@ -118,6 +129,8 @@ export class FilterEditorComponent implements OnInit, AfterViewInit { if (existingRule && existingRule.value == item.id && filterRuleType.id == FILTER_HAS_TAG) { filterRules.splice(filterRules.indexOf(existingRule), 1) + } else if (existingRule && filterRuleType.id == FILTER_HAS_TAG) { + filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == filterRuleType.id), value: item.id}) } else if (existingRule && existingRule.value == item.id) { return } else if (existingRule) { @@ -125,6 +138,10 @@ export class FilterEditorComponent implements OnInit, AfterViewInit { } else { filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == filterRuleType.id), value: item.id}) } + + let dropdown = this.getDropdownByFilterRuleTypeID(filterRuleTypeID) + this.updateDropdownActiveItems(dropdown) + this.filterRules = filterRules this.applySelected() } From 364df5c050c4be7fa72f9ef49bcfedf8869e050e Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Thu, 10 Dec 2020 15:37:56 -0800 Subject: [PATCH 0037/1708] Fix toggling off active items --- .../app/components/filter-editor/filter-editor.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/filter-editor/filter-editor.component.ts index d45732717..d3e8eb244 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.ts @@ -127,9 +127,9 @@ export class FilterEditorComponent implements OnInit, AfterViewInit { let filterRuleType: FilterRuleType = FILTER_RULE_TYPES.find(t => t.id == filterRuleTypeID) let existingRule = filterRules.find(rule => rule.type.id == filterRuleType.id) - if (existingRule && existingRule.value == item.id && filterRuleType.id == FILTER_HAS_TAG) { + if (existingRule && existingRule.value == item.id) { filterRules.splice(filterRules.indexOf(existingRule), 1) - } else if (existingRule && filterRuleType.id == FILTER_HAS_TAG) { + } else if (existingRule && filterRuleType.id == FILTER_HAS_TAG) { filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == filterRuleType.id), value: item.id}) } else if (existingRule && existingRule.value == item.id) { return From 57504b7ee6c8acbda931c85674faa40684fbd132 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Thu, 10 Dec 2020 15:49:00 -0800 Subject: [PATCH 0038/1708] Display tags with color pills --- .../filter-dropdown/filter-dropdown.component.html | 5 ++++- .../filter-dropdown/filter-dropdown.component.ts | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html index 4552406b1..0dc56db2e 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html @@ -10,7 +10,10 @@ <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">{{item.name}}</div> + <div class="mr-1"> + <app-tag *ngIf="display == 'tag'; else displayName" [tag]="item" [clickable]="true" linkTitle="Filter by tag"></app-tag> + <ng-template #displayName>{{item.name}}</ng-template> + </div> <div class="badge bg-primary text-light rounded-pill ml-auto">{{item.document_count}}</div> </button> </ng-container> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts index 070689bd2..1885884cd 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { FilterRuleType, FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_TITLE, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; +import { FilterRuleType, FILTER_HAS_TAG, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; import { ObjectWithId } from 'src/app/data/object-with-id'; @Component({ @@ -21,10 +21,12 @@ export class FilterDropdownComponent implements OnInit { itemsActive: ObjectWithId[] = [] title: string filterText: string + display: string ngOnInit(): void { let filterRuleType: FilterRuleType = FILTER_RULE_TYPES.find(t => t.id == this.filterRuleTypeID) this.title = filterRuleType.name + this.display = filterRuleType.datatype } toggleItem(item: ObjectWithId) { From 4146955f4a7ed8ad0a2a6ddbcff15c9bbb7d871c Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Thu, 10 Dec 2020 15:51:11 -0800 Subject: [PATCH 0039/1708] Shadows! --- .../app/components/document-list/document-list.component.html | 2 +- .../filter-dropdown/filter-dropdown.component.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index 13e1718ce..f8c3445c5 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -47,7 +47,7 @@ <div class="btn-group" ngbDropdown role="group"> <button class="btn btn-sm btn-outline-primary dropdown-toggle" ngbDropdownToggle>Saved Views</button> - <div class="dropdown-menu" ngbDropdownMenu> + <div class="dropdown-menu shadow" ngbDropdownMenu> <ng-container *ngIf="!list.savedViewId"> <button ngbDropdownItem *ngFor="let config of savedViewConfigService.getConfigs()" (click)="loadViewConfig(config)">{{config.title}}</button> <div class="dropdown-divider" *ngIf="savedViewConfigService.getConfigs().length > 0"></div> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html index 0dc56db2e..f3c3b020c 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html @@ -1,6 +1,6 @@ <div class="btn-group" ngbDropdown role="group"> <button class="btn btn-outline-primary btn-sm" id="dropdown{{title}}" ngbDropdownToggle>{{title}}</button> - <div class="dropdown-menu quick-filter" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> + <div class="dropdown-menu quick-filter shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> <div class="list-group list-group-flush"> <input class="list-group-item form-control" type="text" [(ngModel)]="filterText" placeholder="Filter {{title}}"> <ng-container *ngIf="(items | filter: filterText).length > 0"> From 66aa7319aba16860b980f6085abe50b5af3c201a Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Fri, 11 Dec 2020 00:07:26 -0800 Subject: [PATCH 0040/1708] Small text --- .../filter-dropdown/filter-dropdown.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html index f3c3b020c..14d71a393 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html @@ -2,7 +2,7 @@ <button class="btn btn-outline-primary btn-sm" id="dropdown{{title}}" ngbDropdownToggle>{{title}}</button> <div class="dropdown-menu quick-filter shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> <div class="list-group list-group-flush"> - <input class="list-group-item form-control" type="text" [(ngModel)]="filterText" placeholder="Filter {{title}}"> + <input class="list-group-item form-control form-control-sm" type="text" [(ngModel)]="filterText" placeholder="Filter {{title}}"> <ng-container *ngIf="(items | filter: filterText).length > 0"> <button class="list-group-item list-group-item-action d-flex align-items-center" role="menuitem" *ngFor="let item of items | filter: filterText; let i = index" (click)="toggleItem(item)"> <div class="selected-icon mr-1"> @@ -12,7 +12,7 @@ </div> <div class="mr-1"> <app-tag *ngIf="display == 'tag'; else displayName" [tag]="item" [clickable]="true" linkTitle="Filter by tag"></app-tag> - <ng-template #displayName>{{item.name}}</ng-template> + <ng-template #displayName><small>{{item.name}}</small></ng-template> </div> <div class="badge bg-primary text-light rounded-pill ml-auto">{{item.document_count}}</div> </button> From ed480f62e3d67362c56e1a86dfa34c68dd03d132 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Fri, 11 Dec 2020 00:08:06 -0800 Subject: [PATCH 0041/1708] filter-rule-type displayName property --- .../filter-dropdown/filter-dropdown.component.ts | 4 ++-- src-ui/src/app/data/filter-rule-type.ts | 15 ++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts index 1885884cd..0522583c0 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { FilterRuleType, FILTER_HAS_TAG, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; +import { FilterRuleType, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; import { ObjectWithId } from 'src/app/data/object-with-id'; @Component({ @@ -25,7 +25,7 @@ export class FilterDropdownComponent implements OnInit { ngOnInit(): void { let filterRuleType: FilterRuleType = FILTER_RULE_TYPES.find(t => t.id == this.filterRuleTypeID) - this.title = filterRuleType.name + this.title = filterRuleType.displayName this.display = filterRuleType.datatype } diff --git a/src-ui/src/app/data/filter-rule-type.ts b/src-ui/src/app/data/filter-rule-type.ts index 1a174ce57..cf155daf1 100644 --- a/src-ui/src/app/data/filter-rule-type.ts +++ b/src-ui/src/app/data/filter-rule-type.ts @@ -25,23 +25,23 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [ {id: FILTER_ASN, name: "ASN is", filtervar: "archive_serial_number", datatype: "number", multi: false}, - {id: FILTER_CORRESPONDENT, name: "Correspondents", filtervar: "correspondent__id", datatype: "correspondent", multi: false}, - {id: FILTER_DOCUMENT_TYPE, name: "Document types", filtervar: "document_type__id", datatype: "document_type", multi: false}, + {id: FILTER_CORRESPONDENT, name: "Correspondent is", displayName: "Correspondents", filtervar: "correspondent__id", datatype: "correspondent", multi: false}, + {id: FILTER_DOCUMENT_TYPE, name: "Document type is", displayName: "Document types", filtervar: "document_type__id", datatype: "document_type", multi: false}, {id: FILTER_IS_IN_INBOX, name: "Is in Inbox", filtervar: "is_in_inbox", datatype: "boolean", multi: false, default: true}, - {id: FILTER_HAS_TAG, name: "Tags", filtervar: "tags__id__all", datatype: "tag", multi: true}, + {id: FILTER_HAS_TAG, name: "Has tag", displayName: "Tags", filtervar: "tags__id__all", datatype: "tag", multi: true}, {id: FILTER_DOES_NOT_HAVE_TAG, name: "Does not have tag", filtervar: "tags__id__none", datatype: "tag", multi: true}, {id: FILTER_HAS_ANY_TAG, name: "Has any tag", filtervar: "is_tagged", datatype: "boolean", multi: false, default: true}, - {id: FILTER_CREATED_BEFORE, name: "Created before", filtervar: "created__date__lt", datatype: "date", multi: false}, - {id: FILTER_CREATED_AFTER, name: "Created after", filtervar: "created__date__gt", datatype: "date", multi: false}, + {id: FILTER_CREATED_BEFORE, name: "Created before", displayName: "Created", filtervar: "created__date__lt", datatype: "date", multi: false}, + {id: FILTER_CREATED_AFTER, name: "Created after", displayName: "Created", filtervar: "created__date__gt", datatype: "date", multi: false}, {id: FILTER_CREATED_YEAR, name: "Year created is", filtervar: "created__year", datatype: "number", multi: false}, {id: FILTER_CREATED_MONTH, name: "Month created is", filtervar: "created__month", datatype: "number", multi: false}, {id: FILTER_CREATED_DAY, name: "Day created is", filtervar: "created__day", datatype: "number", multi: false}, - {id: FILTER_ADDED_BEFORE, name: "Added before", filtervar: "added__date__lt", datatype: "date", multi: false}, - {id: FILTER_ADDED_AFTER, name: "Added after", filtervar: "added__date__gt", datatype: "date", multi: false}, + {id: FILTER_ADDED_BEFORE, name: "Added before", displayName: "Added", filtervar: "added__date__lt", datatype: "date", multi: false}, + {id: FILTER_ADDED_AFTER, name: "Added after", displayName: "Added", filtervar: "added__date__gt", datatype: "date", multi: false}, {id: FILTER_MODIFIED_BEFORE, name: "Modified before", filtervar: "modified__date__lt", datatype: "date", multi: false}, {id: FILTER_MODIFIED_AFTER, name: "Modified after", filtervar: "modified__date__gt", datatype: "date", multi: false}, @@ -53,5 +53,6 @@ export interface FilterRuleType { filtervar: string datatype: string //number, string, boolean, date multi: boolean + displayName?: string default?: any } From c24bfd4d2bdf72ceed3393a338aca443e67cb695 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Fri, 11 Dec 2020 01:03:05 -0800 Subject: [PATCH 0042/1708] filter-dropdown-date rough implementation --- src-ui/src/app/app.module.ts | 2 + .../filter-dropdown-date.component.html | 41 ++++++++++++++ .../filter-dropdown-date.component.scss | 3 ++ .../filter-dropdown-date.component.spec.ts | 25 +++++++++ .../filter-dropdown-date.component.ts | 53 +++++++++++++++++++ .../filter-editor.component.html | 13 +---- .../filter-editor/filter-editor.component.ts | 7 +-- 7 files changed, 130 insertions(+), 14 deletions(-) create mode 100644 src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.html create mode 100644 src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.scss create mode 100644 src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.spec.ts create mode 100644 src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.ts diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index 6a847494a..394e3ba58 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -29,6 +29,7 @@ import { AppFrameComponent } from './components/app-frame/app-frame.component'; import { ToastsComponent } from './components/common/toasts/toasts.component'; import { FilterEditorComponent } from './components/filter-editor/filter-editor.component'; import { FilterDropdownComponent } from './components/filter-editor/filter-dropdown/filter-dropdown.component'; +import { FilterDropdownDateComponent } from './components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.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 { NgxFileDropModule } from 'ngx-file-drop'; @@ -76,6 +77,7 @@ import { FilterPipe } from './pipes/filter.pipe'; ToastsComponent, FilterEditorComponent, FilterDropdownComponent, + FilterDropdownDateComponent, DocumentCardLargeComponent, DocumentCardSmallComponent, TextComponent, diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.html new file mode 100644 index 000000000..74d508390 --- /dev/null +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.html @@ -0,0 +1,41 @@ + <div class="btn-group" ngbDropdown role="group"> + <button class="btn btn-outline-primary btn-sm" id="dropdown{{title}}" ngbDropdownToggle>{{title}}</button> + <div class="dropdown-menu date-filter shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> + <div class="list-group list-group-flush"> + <div class="list-group-item d-flex flex-column align-items-start"> + <button class="btn btn-sm btn-link pl-0" (click)="setQuickFilter(7)">Last 7 days</button> + <button class="btn btn-sm btn-link pl-0" (click)="setQuickFilter(30)">Last 30 days</button> + <button class="btn btn-sm btn-link pl-0" *ngIf="showMonth" (click)="setQuickFilter('month')">This month</button> + <button class="btn btn-sm btn-link pl-0" *ngIf="showYear" (click)="setQuickFilter('year')">This year</button> + </div> + <div class="list-group-item d-flex flex-column align-items-start" role="menuitem"> + <div class="mb-1"><small>Before</small></div> + <div class="input-group input-group-sm"> + <input class="form-control" type="text" placeholder="yyyy-mm-dd" name="before" [(ngModel)]="dateBefore" ngbDatepicker #dpBefore="ngbDatepicker"> + <div class="input-group-append"> + <button class="btn btn-outline-secondary btn-sm" (click)="dpBefore.toggle()" type="button"> + <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-calendar-date" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> + <path fill-rule="evenodd" d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/> + <path d="M6.445 11.688V6.354h-.633A12.6 12.6 0 0 0 4.5 7.16v.695c.375-.257.969-.62 1.258-.777h.012v4.61h.675zm1.188-1.305c.047.64.594 1.406 1.703 1.406 1.258 0 2-1.066 2-2.871 0-1.934-.781-2.668-1.953-2.668-.926 0-1.797.672-1.797 1.809 0 1.16.824 1.77 1.676 1.77.746 0 1.23-.376 1.383-.79h.027c-.004 1.316-.461 2.164-1.305 2.164-.664 0-1.008-.45-1.05-.82h-.684zm2.953-2.317c0 .696-.559 1.18-1.184 1.18-.601 0-1.144-.383-1.144-1.2 0-.823.582-1.21 1.168-1.21.633 0 1.16.398 1.16 1.23z"/> + </svg> + </button> + </div> + </div> + </div> + <div class="list-group-item d-flex flex-column align-items-start" role="menuitem"> + <div class="mb-1"><small>After</small></div> + <div class="input-group"> + <input class="form-control form-control-sm" type="text" placeholder="yyyy-mm-dd" name="after" [(ngModel)]="dateAfter" ngbDatepicker #dpAfter="ngbDatepicker"> + <div class="input-group-append"> + <button class="btn btn-outline-secondary btn-sm" (click)="dpAfter.toggle()" type="button"> + <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-calendar-date" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> + <path fill-rule="evenodd" d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/> + <path d="M6.445 11.688V6.354h-.633A12.6 12.6 0 0 0 4.5 7.16v.695c.375-.257.969-.62 1.258-.777h.012v4.61h.675zm1.188-1.305c.047.64.594 1.406 1.703 1.406 1.258 0 2-1.066 2-2.871 0-1.934-.781-2.668-1.953-2.668-.926 0-1.797.672-1.797 1.809 0 1.16.824 1.77 1.676 1.77.746 0 1.23-.376 1.383-.79h.027c-.004 1.316-.461 2.164-1.305 2.164-.664 0-1.008-.45-1.05-.82h-.684zm2.953-2.317c0 .696-.559 1.18-1.184 1.18-.601 0-1.144-.383-1.144-1.2 0-.823.582-1.21 1.168-1.21.633 0 1.16.398 1.16 1.23z"/> + </svg> + </button> + </div> + </div> + </div> + </div> + </div> +</div> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.scss b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.scss new file mode 100644 index 000000000..67edb9bf8 --- /dev/null +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.scss @@ -0,0 +1,3 @@ +.date-filter { + min-width: 250px; +} diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.spec.ts b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.spec.ts new file mode 100644 index 000000000..6bf59e2e7 --- /dev/null +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FilterDropdownDateComponent } from './filter-dropdown-date.component'; + +describe('FilterDropdownDateComponent', () => { + let component: FilterDropdownDateComponent; + let fixture: ComponentFixture<FilterDropdownDateComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ FilterDropdownDateComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FilterDropdownDateComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.ts new file mode 100644 index 000000000..37ea2cd09 --- /dev/null +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.ts @@ -0,0 +1,53 @@ +import { Component, EventEmitter, Input, OnInit, Output, ElementRef, ViewChild } from '@angular/core'; +import { FilterRuleType, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; +import { ObjectWithId } from 'src/app/data/object-with-id'; +import { FilterDropdownComponent } from '../filter-dropdown.component' +import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'app-filter-dropdown-date', + templateUrl: './filter-dropdown-date.component.html', + styleUrls: ['./filter-dropdown-date.component.scss'] +}) +export class FilterDropdownDateComponent extends FilterDropdownComponent { + + @Input() + filterRuleTypeIDs: number[] = [] + + @Output() + selected = new EventEmitter() + + filterRuleTypes: FilterRuleType[] = [] + showYear: boolean = false + showMonth: boolean = false + dateAfter: NgbDateStruct + dateBefore: NgbDateStruct + + ngOnInit(): void { + this.filterRuleTypes = this.filterRuleTypeIDs.map(id => FILTER_RULE_TYPES.find(rt => rt.id == id)) + this.filterRuleTypeID = this.filterRuleTypeIDs[0] + super.ngOnInit() + + this.showYear = this.filterRuleTypes.find(rt => rt.filtervar.indexOf('year') > -1) !== undefined + this.showMonth = this.filterRuleTypes.find(rt => rt.filtervar.indexOf('month') > -1) !== undefined + } + + setQuickFilter(range: any) { + this.dateAfter = this.dateBefore = undefined + switch (typeof range) { + case 'number': + let date = new Date(); + date.setDate(date.getDate() - range) + this.dateAfter = { year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate() } + break; + + case 'string': + let filterRuleType = this.filterRuleTypes.find(rt => rt.filtervar.indexOf(range) > -1) + console.log(range); + break; + + default: + break; + } + } +} diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.html b/src-ui/src/app/components/filter-editor/filter-editor.component.html index 153f32644..d3473337b 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.html +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.html @@ -6,18 +6,9 @@ <input class="form-control form-control-sm" type="text" [(ngModel)]="filterText" placeholder="Title" #filterTextInput> </div> - <app-filter-dropdown class="col-auto" *ngFor="let quickFilterRuleTypeID of quickFilterRuleTypeIDs" - [filterRuleTypeID]="quickFilterRuleTypeID" - (toggle)="toggleFilterByItem($event, quickFilterRuleTypeID)"> - </app-filter-dropdown> + <app-filter-dropdown class="col-auto" *ngFor="let quickFilterRuleTypeID of quickFilterRuleTypeIDs" [filterRuleTypeID]="quickFilterRuleTypeID" (toggle)="toggleFilterByItem($event, quickFilterRuleTypeID)"></app-filter-dropdown> - <div class="btn-group col-auto" ngbDropdown role="group"> - <button class="btn btn-outline-primary btn-sm" id="dropdownCreated" ngbDropdownToggle>Created</button> - </div> - - <div class="btn-group col-auto" ngbDropdown role="group"> - <button class="btn btn-outline-primary btn-sm" id="dropdownAdded" ngbDropdownToggle>Added</button> - </div> + <app-filter-dropdown-date class="col-auto" *ngFor="let dateAddedFilterRuleTypeID of dateAddedFilterRuleTypeIDs" [filterRuleTypeIDs]="dateAddedFilterRuleTypeID" (toggle)="toggleFilterByItem($event, quickFilterRuleTypeID)"></app-filter-dropdown-date> <button class="btn btn-link btn-sm" [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"> diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/filter-editor/filter-editor.component.ts index d3e8eb244..f3d77d44b 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.ts @@ -1,6 +1,6 @@ import { Component, EventEmitter, Input, OnInit, Output, ElementRef, AfterViewInit, QueryList, ViewChild, ViewChildren } from '@angular/core'; import { FilterRule } from 'src/app/data/filter-rule'; -import { FilterRuleType, FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_TITLE, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; +import { FilterRuleType, FILTER_RULE_TYPES, FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_TITLE, FILTER_ADDED_BEFORE, FILTER_ADDED_AFTER, FILTER_CREATED_BEFORE, FILTER_CREATED_AFTER, FILTER_CREATED_YEAR, FILTER_CREATED_MONTH, FILTER_CREATED_DAY } from 'src/app/data/filter-rule-type'; import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'; import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'; import { PaperlessTag } from 'src/app/data/paperless-tag'; @@ -31,10 +31,11 @@ export class FilterEditorComponent implements OnInit, AfterViewInit { @Output() apply = new EventEmitter() - @ViewChild('filterTextInput') input: ElementRef; + @ViewChild('filterTextInput') filterTextInput: ElementRef; @ViewChildren(FilterDropdownComponent) quickFilterDropdowns!: QueryList<FilterDropdownComponent>; quickFilterRuleTypeIDs: number[] = [FILTER_HAS_TAG, FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE] + dateAddedFilterRuleTypeIDs: any[] = [[FILTER_ADDED_BEFORE, FILTER_ADDED_AFTER], [FILTER_CREATED_BEFORE, FILTER_CREATED_AFTER, FILTER_CREATED_YEAR, FILTER_CREATED_MONTH, FILTER_CREATED_DAY]] correspondents: PaperlessCorrespondent[] = [] tags: PaperlessTag[] = [] @@ -53,7 +54,7 @@ export class FilterEditorComponent implements OnInit, AfterViewInit { } ngAfterViewInit() { - fromEvent(this.input.nativeElement,'keyup') + fromEvent(this.filterTextInput.nativeElement,'keyup') .pipe( debounceTime(150), distinctUntilChanged(), From a4a08aa667dd43c50d43910129da7a0676505019 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Fri, 11 Dec 2020 01:16:58 -0800 Subject: [PATCH 0043/1708] auto-select list filter field & clear on close --- .../filter-dropdown.component.html | 4 ++-- .../filter-dropdown/filter-dropdown.component.ts | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html index 14d71a393..fad1b6663 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html @@ -1,8 +1,8 @@ - <div class="btn-group" ngbDropdown role="group"> + <div class="btn-group" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)"> <button class="btn btn-outline-primary btn-sm" id="dropdown{{title}}" ngbDropdownToggle>{{title}}</button> <div class="dropdown-menu quick-filter shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> <div class="list-group list-group-flush"> - <input class="list-group-item form-control form-control-sm" type="text" [(ngModel)]="filterText" placeholder="Filter {{title}}"> + <input class="list-group-item form-control form-control-sm" type="text" [(ngModel)]="filterText" placeholder="Filter {{title}}" #filterTextInput> <ng-container *ngIf="(items | filter: filterText).length > 0"> <button class="list-group-item list-group-item-action d-flex align-items-center" role="menuitem" *ngFor="let item of items | filter: filterText; let i = index" (click)="toggleItem(item)"> <div class="selected-icon mr-1"> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts index 0522583c0..625c72be8 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnInit, Output, ElementRef, ViewChild } from '@angular/core'; import { FilterRuleType, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; import { ObjectWithId } from 'src/app/data/object-with-id'; @@ -17,6 +17,8 @@ export class FilterDropdownComponent implements OnInit { @Output() toggle = new EventEmitter() + @ViewChild('filterTextInput') filterTextInput: ElementRef + items: ObjectWithId[] = [] itemsActive: ObjectWithId[] = [] title: string @@ -29,7 +31,17 @@ export class FilterDropdownComponent implements OnInit { this.display = filterRuleType.datatype } - toggleItem(item: ObjectWithId) { + toggleItem(item: ObjectWithId): void { this.toggle.emit(item) } + + dropdownOpenChange(open: boolean): void { + if (open) { + setTimeout(() => { + this.filterTextInput.nativeElement.focus(); + }, 0); + } else { + this.filterText = '' + } + } } From 0b4c860354c45fb188f47d808e59fe5a56fff1c2 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Fri, 11 Dec 2020 01:19:49 -0800 Subject: [PATCH 0044/1708] refactoring --- .../filter-dropdown/filter-dropdown.component.html | 6 +++--- .../filter-dropdown/filter-dropdown.component.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html index fad1b6663..6e73b31a7 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html @@ -2,9 +2,9 @@ <button class="btn btn-outline-primary btn-sm" id="dropdown{{title}}" ngbDropdownToggle>{{title}}</button> <div class="dropdown-menu quick-filter shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> <div class="list-group list-group-flush"> - <input class="list-group-item form-control form-control-sm" type="text" [(ngModel)]="filterText" placeholder="Filter {{title}}" #filterTextInput> - <ng-container *ngIf="(items | filter: filterText).length > 0"> - <button class="list-group-item list-group-item-action d-flex align-items-center" role="menuitem" *ngFor="let item of items | filter: filterText; let i = index" (click)="toggleItem(item)"> + <input class="list-group-item form-control form-control-sm" type="text" [(ngModel)]="listFilterText" placeholder="Filter {{title}}" #listFilterTextInput> + <ng-container *ngIf="(items | filter: listFilterText).length > 0"> + <button class="list-group-item list-group-item-action d-flex align-items-center" role="menuitem" *ngFor="let item of items | filter: listFilterText; let i = index" (click)="toggleItem(item)"> <div class="selected-icon mr-1"> <svg *ngIf="itemsActive.includes(item)" 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"/> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts index 625c72be8..a57543424 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts @@ -17,12 +17,12 @@ export class FilterDropdownComponent implements OnInit { @Output() toggle = new EventEmitter() - @ViewChild('filterTextInput') filterTextInput: ElementRef + @ViewChild('listFilterTextInput') listFilterTextInput: ElementRef items: ObjectWithId[] = [] itemsActive: ObjectWithId[] = [] title: string - filterText: string + listFilterText: string display: string ngOnInit(): void { @@ -38,7 +38,7 @@ export class FilterDropdownComponent implements OnInit { dropdownOpenChange(open: boolean): void { if (open) { setTimeout(() => { - this.filterTextInput.nativeElement.focus(); + this.listFilterTextInput.nativeElement.focus(); }, 0); } else { this.filterText = '' From a37796d0cf7caf1f5f53727521d057188f768b25 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Fri, 11 Dec 2020 01:40:42 -0800 Subject: [PATCH 0045/1708] Allow enter key to toggle items in filtered list if single item remains --- src-ui/src/app/app.module.ts | 3 ++- .../filter-dropdown/filter-dropdown.component.html | 6 +++--- .../filter-dropdown/filter-dropdown.component.ts | 10 ++++++++-- .../filter-editor/filter-editor.component.ts | 3 --- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index 394e3ba58..4c24123e6 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -112,7 +112,8 @@ import { FilterPipe } from './pipes/filter.pipe'; provide: HTTP_INTERCEPTORS, useClass: CsrfInterceptor, multi: true - } + }, + FilterPipe ], bootstrap: [AppComponent] }) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html index 6e73b31a7..b43826fb2 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html @@ -2,9 +2,9 @@ <button class="btn btn-outline-primary btn-sm" id="dropdown{{title}}" ngbDropdownToggle>{{title}}</button> <div class="dropdown-menu quick-filter shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> <div class="list-group list-group-flush"> - <input class="list-group-item form-control form-control-sm" type="text" [(ngModel)]="listFilterText" placeholder="Filter {{title}}" #listFilterTextInput> - <ng-container *ngIf="(items | filter: listFilterText).length > 0"> - <button class="list-group-item list-group-item-action d-flex align-items-center" role="menuitem" *ngFor="let item of items | filter: listFilterText; let i = index" (click)="toggleItem(item)"> + <input class="list-group-item form-control form-control-sm" type="text" [(ngModel)]="filterText" placeholder="Filter {{title}}" (keyup.enter)="listFilterEnter()" #listFilterTextInput> + <ng-container *ngIf="(items | filter: filterText).length > 0"> + <button class="list-group-item list-group-item-action d-flex align-items-center" role="menuitem" *ngFor="let item of items | filter: filterText; let i = index" (click)="toggleItem(item)"> <div class="selected-icon mr-1"> <svg *ngIf="itemsActive.includes(item)" 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"/> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts index a57543424..6f346d4b3 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts @@ -1,6 +1,7 @@ import { Component, EventEmitter, Input, OnInit, Output, ElementRef, ViewChild } from '@angular/core'; import { FilterRuleType, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; import { ObjectWithId } from 'src/app/data/object-with-id'; +import { FilterPipe } from 'src/app/pipes/filter.pipe'; @Component({ selector: 'app-filter-dropdown', @@ -9,7 +10,7 @@ import { ObjectWithId } from 'src/app/data/object-with-id'; }) export class FilterDropdownComponent implements OnInit { - constructor() { } + constructor(private filterPipe: FilterPipe) { } @Input() filterRuleTypeID: number @@ -22,7 +23,7 @@ export class FilterDropdownComponent implements OnInit { items: ObjectWithId[] = [] itemsActive: ObjectWithId[] = [] title: string - listFilterText: string + filterText: string display: string ngOnInit(): void { @@ -44,4 +45,9 @@ export class FilterDropdownComponent implements OnInit { this.filterText = '' } } + + listFilterEnter(): void { + let filtered = this.filterPipe.transform(this.items, this.filterText) + if (filtered.length == 1) this.toggleItem(filtered.shift()) + } } diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/filter-editor/filter-editor.component.ts index f3d77d44b..93a91473f 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.ts @@ -42,9 +42,6 @@ export class FilterEditorComponent implements OnInit, AfterViewInit { documentTypes: PaperlessDocumentType[] = [] filterText: string - filterTagsText: string - filterCorrespondentsText: string - filterDocumentTypesText: string ngOnInit(): void { this.updateTextFilterInput() From fbb3a069cd20118a75b4cb5b9fe057d474d1fb83 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Fri, 11 Dec 2020 14:27:54 +0100 Subject: [PATCH 0046/1708] add bulk editing methods --- src/documents/bulk_edit.py | 90 +++++++++++++++++++++++--------------- src/documents/tasks.py | 7 +++ 2 files changed, 62 insertions(+), 35 deletions(-) diff --git a/src/documents/bulk_edit.py b/src/documents/bulk_edit.py index f80c55c58..1349f9d54 100644 --- a/src/documents/bulk_edit.py +++ b/src/documents/bulk_edit.py @@ -1,49 +1,69 @@ -from documents.models import Document, Correspondent +from django.db.models import Q +from django_q.tasks import async_task -methods_supported = [ - "set_correspondent" -] +from documents.models import Document, Correspondent, DocumentType -def validate_data(data): - if 'ids' not in data or not isinstance(data['ids'], list): - raise ValueError() - ids = data['ids'] - if not all([isinstance(i, int) for i in ids]): - raise ValueError() - count = Document.objects.filter(pk__in=ids).count() - if not count == len(ids): - raise Document.DoesNotExist() +def set_correspondent(doc_ids, correspondent): + if correspondent: + correspondent = Correspondent.objects.get(id=correspondent) - if 'method' not in data or not isinstance(data['method'], str): - raise ValueError() - method = data['method'] - if method not in methods_supported: - raise ValueError() + qs = Document.objects.filter(Q(id__in=doc_ids) & ~Q(correspondent=correspondent)) + affected_docs = [doc.id for doc in qs] + qs.update(correspondent=correspondent) - if 'args' not in data or not isinstance(data['args'], list): - raise ValueError() - parameters = data['args'] + async_task("documents.tasks.bulk_rename_files", affected_docs) - return ids, method, parameters + return "OK" -def perform_bulk_edit(data): - ids, method, args = validate_data(data) +def set_document_type(doc_ids, document_type): + if document_type: + document_type = DocumentType.objects.get(id=document_type) - getattr(__file__, method)(ids, args) + qs = Document.objects.filter(Q(id__in=doc_ids) & ~Q(document_type=document_type)) + affected_docs = [doc.id for doc in qs] + qs.update(document_type=document_type) + + async_task("documents.tasks.bulk_rename_files", affected_docs) + + return "OK" -def set_correspondent(ids, args): - if not len(args) == 1: - raise ValueError() +def add_tag(doc_ids, tag): - if not args[0]: - correspondent = None - else: - if not isinstance(args[0], int): - raise ValueError() + qs = Document.objects.filter(Q(id__in=doc_ids) & ~Q(tags__id=tag)) + affected_docs = [doc.id for doc in qs] - correspondent = Correspondent.objects.get(args[0]) + DocumentTagRelationship = Document.tags.through - Document.objects.filter(id__in=ids).update(correspondent=correspondent) + DocumentTagRelationship.objects.bulk_create([ + DocumentTagRelationship(document_id=doc, tag_id=tag) for doc in affected_docs + ]) + + async_task("documents.tasks.bulk_rename_files", affected_docs) + + return "OK" + + +def remove_tag(doc_ids, tag): + + qs = Document.objects.filter(Q(id__in=doc_ids) & Q(tags__id=tag)) + affected_docs = [doc.id for doc in qs] + + DocumentTagRelationship = Document.tags.through + + DocumentTagRelationship.objects.filter( + Q(document_id__in=affected_docs) & + Q(tag_id=tag) + ).delete() + + async_task("documents.tasks.bulk_rename_files", affected_docs) + + return "OK" + + +def delete(doc_ids): + Document.objects.filter(id__in=doc_ids).delete() + + return "OK" diff --git a/src/documents/tasks.py b/src/documents/tasks.py index 8c9b00dd6..af4c91448 100644 --- a/src/documents/tasks.py +++ b/src/documents/tasks.py @@ -2,6 +2,7 @@ import logging import tqdm from django.conf import settings +from django.db.models.signals import post_save from whoosh.writing import AsyncWriter from documents import index, sanity_checker @@ -87,3 +88,9 @@ def sanity_check(): raise SanityFailedError(messages) else: return "No issues detected." + + +def bulk_rename_files(ids): + qs = Document.objects.filter(id__in=ids) + for doc in qs: + post_save.send(Document, instance=doc, created=False) From 66d6d29c239670575c0dc0c100234764f8eeead9 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Fri, 11 Dec 2020 14:29:43 +0100 Subject: [PATCH 0047/1708] add support to the documents api to only serve selected fields --- src/documents/serialisers.py | 24 +++++++++++++++++++++++- src/documents/views.py | 11 +++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index faded5125..69cbb4092 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -7,6 +7,28 @@ from .models import Correspondent, Tag, Document, Log, DocumentType from .parsers import is_mime_type_supported +# https://www.django-rest-framework.org/api-guide/serializers/#example +class DynamicFieldsModelSerializer(serializers.ModelSerializer): + """ + A ModelSerializer that takes an additional `fields` argument that + controls which fields should be displayed. + """ + + def __init__(self, *args, **kwargs): + # Don't pass the 'fields' arg up to the superclass + fields = kwargs.pop('fields', None) + + # Instantiate the superclass normally + super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs) + + if fields is not None: + # Drop any fields that are not specified in the `fields` argument. + allowed = set(fields) + existing = set(self.fields) + for field_name in existing - allowed: + self.fields.pop(field_name) + + class CorrespondentSerializer(serializers.ModelSerializer): document_count = serializers.IntegerField(read_only=True) @@ -90,7 +112,7 @@ class DocumentTypeField(serializers.PrimaryKeyRelatedField): return DocumentType.objects.all() -class DocumentSerializer(serializers.ModelSerializer): +class DocumentSerializer(DynamicFieldsModelSerializer): correspondent = CorrespondentField(allow_null=True) tags = TagsField(many=True) diff --git a/src/documents/views.py b/src/documents/views.py index b1d93b77b..10cb30eb3 100755 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -131,6 +131,17 @@ class DocumentViewSet(RetrieveModelMixin, "added", "archive_serial_number") + def get_serializer(self, *args, **kwargs): + fields_param = self.request.query_params.get('fields', None) + if fields_param: + fields = fields_param.split(",") + else: + fields = None + serializer_class = self.get_serializer_class() + kwargs.setdefault('context', self.get_serializer_context()) + kwargs.setdefault('fields', fields) + return serializer_class(*args, **kwargs) + def update(self, request, *args, **kwargs): response = super(DocumentViewSet, self).update( request, *args, **kwargs) From 4b0027797a6c70879bc9ce2850c645ca136931c7 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Fri, 11 Dec 2020 14:30:18 +0100 Subject: [PATCH 0048/1708] bulk edit view --- src/documents/serialisers.py | 22 ++++++++++++++++++---- src/documents/views.py | 11 +++++++++++ src/paperless/urls.py | 7 ++++++- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 69cbb4092..5418ec0fb 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -3,6 +3,7 @@ from django.utils.text import slugify from rest_framework import serializers from rest_framework.fields import SerializerMethodField +from . import bulk_edit from .models import Correspondent, Tag, Document, Log, DocumentType from .parsers import is_mime_type_supported @@ -164,11 +165,10 @@ class LogSerializer(serializers.ModelSerializer): class BulkEditSerializer(serializers.Serializer): - documents = serializers.PrimaryKeyRelatedField( - many=True, + documents = serializers.ListField( + child=serializers.IntegerField(), label="Documents", - write_only=True, - queryset=Document.objects.all() + write_only=True ) method = serializers.ChoiceField( @@ -185,6 +185,20 @@ class BulkEditSerializer(serializers.Serializer): parameters = serializers.DictField(allow_empty=True) + def validate_method(self, method): + if method == "set_correspondent": + return bulk_edit.set_correspondent + elif method == "set_document_type": + return bulk_edit.set_document_type + elif method == "add_tag": + return bulk_edit.add_tag + elif method == "remove_tag": + return bulk_edit.remove_tag + elif method == "delete": + return bulk_edit.delete + else: + raise serializers.ValidationError("Unsupported method.") + def validate(self, attrs): return attrs diff --git a/src/documents/views.py b/src/documents/views.py index 10cb30eb3..4ce78348e 100755 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -278,6 +278,17 @@ class BulkEditView(APIView): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) + method = serializer.validated_data.get("method") + parameters = serializer.validated_data.get("parameters") + documents = serializer.validated_data.get("documents") + + try: + # TODO: parameter validation + result = method(documents, **parameters) + return Response({"result": result}) + except Exception as e: + return HttpResponseBadRequest(str(e)) + class PostDocumentView(APIView): diff --git a/src/paperless/urls.py b/src/paperless/urls.py index 9b390b139..dc416f05f 100755 --- a/src/paperless/urls.py +++ b/src/paperless/urls.py @@ -17,7 +17,8 @@ from documents.views import ( IndexView, SearchAutoCompleteView, StatisticsView, - PostDocumentView + PostDocumentView, + BulkEditView ) from paperless.views import FaviconView @@ -50,6 +51,10 @@ urlpatterns = [ re_path(r"^documents/post_document/", PostDocumentView.as_view(), name="post_document"), + + re_path(r"^documents/bulk_edit/", BulkEditView.as_view(), + name="bulk_edit"), + path('token/', views.obtain_auth_token) ] + api_router.urls)), From 63a58ccc38e2da9ed5a02fb3e650c9fb61ba1c2b Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Fri, 11 Dec 2020 14:30:59 +0100 Subject: [PATCH 0049/1708] a simple dialog that selects tags/correspondents/types --- src-ui/src/app/app.module.ts | 4 ++- .../select-dialog.component.html | 15 ++++++++ .../select-dialog.component.scss | 0 .../select-dialog.component.spec.ts | 25 ++++++++++++++ .../select-dialog/select-dialog.component.ts | 34 +++++++++++++++++++ 5 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 src-ui/src/app/components/common/select-dialog/select-dialog.component.html create mode 100644 src-ui/src/app/components/common/select-dialog/select-dialog.component.scss create mode 100644 src-ui/src/app/components/common/select-dialog/select-dialog.component.spec.ts create mode 100644 src-ui/src/app/components/common/select-dialog/select-dialog.component.ts diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index ad12c9c47..af66993c6 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -48,6 +48,7 @@ import { WidgetFrameComponent } from './components/dashboard/widgets/widget-fram import { WelcomeWidgetComponent } from './components/dashboard/widgets/welcome-widget/welcome-widget.component'; import { YesNoPipe } from './pipes/yes-no.pipe'; import { FileSizePipe } from './pipes/file-size.pipe'; +import { SelectDialogComponent } from './components/common/select-dialog/select-dialog.component'; @NgModule({ declarations: [ @@ -88,7 +89,8 @@ import { FileSizePipe } from './pipes/file-size.pipe'; WidgetFrameComponent, WelcomeWidgetComponent, YesNoPipe, - FileSizePipe + FileSizePipe, + SelectDialogComponent ], imports: [ BrowserModule, diff --git a/src-ui/src/app/components/common/select-dialog/select-dialog.component.html b/src-ui/src/app/components/common/select-dialog/select-dialog.component.html new file mode 100644 index 000000000..8bde38d61 --- /dev/null +++ b/src-ui/src/app/components/common/select-dialog/select-dialog.component.html @@ -0,0 +1,15 @@ +<div class="modal-header"> + <h4 class="modal-title" id="modal-basic-title">{{title}}</h4> + <button type="button" class="close" aria-label="Close" (click)="cancelClicked()"> + <span aria-hidden="true">×</span> + </button> +</div> +<div class="modal-body"> + + <app-input-select [items]="objects" [title]="message" [(ngModel)]="selected"></app-input-select> + +</div> +<div class="modal-footer"> + <button type="button" class="btn btn-outline-dark" (click)="cancelClicked()">Cancel</button> + <button type="button" class="btn btn-primary" (click)="selectClicked.emit(selected)">Select</button> +</div> \ No newline at end of file diff --git a/src-ui/src/app/components/common/select-dialog/select-dialog.component.scss b/src-ui/src/app/components/common/select-dialog/select-dialog.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src-ui/src/app/components/common/select-dialog/select-dialog.component.spec.ts b/src-ui/src/app/components/common/select-dialog/select-dialog.component.spec.ts new file mode 100644 index 000000000..3810bcbea --- /dev/null +++ b/src-ui/src/app/components/common/select-dialog/select-dialog.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SelectDialogComponent } from './select-dialog.component'; + +describe('SelectDialogComponent', () => { + let component: SelectDialogComponent; + let fixture: ComponentFixture<SelectDialogComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SelectDialogComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SelectDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src-ui/src/app/components/common/select-dialog/select-dialog.component.ts b/src-ui/src/app/components/common/select-dialog/select-dialog.component.ts new file mode 100644 index 000000000..76b23491c --- /dev/null +++ b/src-ui/src/app/components/common/select-dialog/select-dialog.component.ts @@ -0,0 +1,34 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { ObjectWithId } from 'src/app/data/object-with-id'; + +@Component({ + selector: 'app-select-dialog', + templateUrl: './select-dialog.component.html', + styleUrls: ['./select-dialog.component.scss'] +}) + +export class SelectDialogComponent implements OnInit { + constructor(public activeModal: NgbActiveModal) { } + + @Output() + public selectClicked = new EventEmitter() + + @Input() + title = "Select" + + @Input() + message = "Please select an object" + + @Input() + objects: ObjectWithId[] = [] + + selected: number + + ngOnInit(): void { + } + + cancelClicked() { + this.activeModal.close() + } +} From 2c702eb568dff1d4d0a3e5cff0b7f07e96abc2a6 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Fri, 11 Dec 2020 14:46:38 +0100 Subject: [PATCH 0050/1708] fixed some issues with the data access service, support for requesting all IDs of filtered documents (required for selection) --- .../app/services/document-list-view.service.ts | 2 +- .../services/rest/abstract-paperless-service.ts | 6 +++--- src-ui/src/app/services/rest/document.service.ts | 16 +++++++++++----- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src-ui/src/app/services/document-list-view.service.ts b/src-ui/src/app/services/document-list-view.service.ts index 811ac3c4b..149096591 100644 --- a/src-ui/src/app/services/document-list-view.service.ts +++ b/src-ui/src/app/services/document-list-view.service.ts @@ -90,7 +90,7 @@ export class DocumentListViewService { reload(onFinish?) { this.isReloading = true - this.documentService.list( + this.documentService.listFiltered( this.currentPage, this.currentPageSize, this.view.sortField, diff --git a/src-ui/src/app/services/rest/abstract-paperless-service.ts b/src-ui/src/app/services/rest/abstract-paperless-service.ts index 3feed320e..d05b4aced 100644 --- a/src-ui/src/app/services/rest/abstract-paperless-service.ts +++ b/src-ui/src/app/services/rest/abstract-paperless-service.ts @@ -54,9 +54,9 @@ export abstract class AbstractPaperlessService<T extends ObjectWithId> { private _listAll: Observable<Results<T>> - listAll(ordering?: string, extraParams?): Observable<Results<T>> { + listAll(sortField?: string, sortDirection?: string, extraParams?): Observable<Results<T>> { if (!this._listAll) { - this._listAll = this.list(1, 100000, ordering, extraParams).pipe( + this._listAll = this.list(1, 100000, sortField, sortDirection, extraParams).pipe( publishReplay(1), refCount() ) @@ -94,4 +94,4 @@ export abstract class AbstractPaperlessService<T extends ObjectWithId> { this._listAll = null return this.http.put<T>(this.getResourceUrl(o.id), o) } -} \ No newline at end of file +} diff --git a/src-ui/src/app/services/rest/document.service.ts b/src-ui/src/app/services/rest/document.service.ts index 4979ba8be..99ec1f3ee 100644 --- a/src-ui/src/app/services/rest/document.service.ts +++ b/src-ui/src/app/services/rest/document.service.ts @@ -64,8 +64,8 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument> return doc } - list(page?: number, pageSize?: number, sortField?: string, sortDirection?: string, filterRules?: FilterRule[]): Observable<Results<PaperlessDocument>> { - return super.list(page, pageSize, sortField, sortDirection, this.filterRulesToQueryParams(filterRules)).pipe( + listFiltered(page?: number, pageSize?: number, sortField?: string, sortDirection?: string, filterRules?: FilterRule[], extraParams = {}): Observable<Results<PaperlessDocument>> { + return this.list(page, pageSize, sortField, sortDirection, Object.assign(extraParams, this.filterRulesToQueryParams(filterRules))).pipe( map(results => { results.results.forEach(doc => this.addObservablesToDocument(doc)) return results @@ -73,6 +73,12 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument> ) } + listAllFilteredIds(filterRules?: FilterRule[]): Observable<number[]> { + return this.listFiltered(1, 100000, null, null, filterRules, {"fields": "id"}).pipe( + map(response => response.results.map(doc => doc.id)) + ) + } + getPreviewUrl(id: number, original: boolean = false): string { let url = this.getResourceUrl(id, 'preview') if (original) { @@ -101,11 +107,11 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument> return this.http.get<PaperlessDocumentMetadata>(this.getResourceUrl(id, 'metadata')) } - bulk_edit(ids: number[], method: string, args: any[]) { + bulkEdit(ids: number[], method: string, args: any) { return this.http.post(this.getResourceUrl(null, 'bulk_edit'), { - 'ids': ids, + 'documents': ids, 'method': method, - 'args': args + 'parameters': args }) } From a8f27f79ddf517b9bcc24a280c7b0077e17f2a9f Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Fri, 11 Dec 2020 14:47:33 +0100 Subject: [PATCH 0051/1708] delete dialog: delay enable delete button --- .../delete-dialog/delete-dialog.component.html | 2 +- .../delete-dialog/delete-dialog.component.ts | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src-ui/src/app/components/common/delete-dialog/delete-dialog.component.html b/src-ui/src/app/components/common/delete-dialog/delete-dialog.component.html index 2de507549..52287fc69 100644 --- a/src-ui/src/app/components/common/delete-dialog/delete-dialog.component.html +++ b/src-ui/src/app/components/common/delete-dialog/delete-dialog.component.html @@ -10,5 +10,5 @@ </div> <div class="modal-footer"> <button type="button" class="btn btn-outline-dark" (click)="cancelClicked()">Cancel</button> - <button type="button" class="btn btn-danger" (click)="deleteClicked.emit()">Delete</button> + <button type="button" class="btn btn-danger" (click)="deleteClicked.emit()" [disabled]="!deleteButtonEnabled">Delete<span *ngIf="!deleteButtonEnabled"> ({{seconds}})</span></button> </div> \ No newline at end of file diff --git a/src-ui/src/app/components/common/delete-dialog/delete-dialog.component.ts b/src-ui/src/app/components/common/delete-dialog/delete-dialog.component.ts index 20114c78c..38ec93bae 100644 --- a/src-ui/src/app/components/common/delete-dialog/delete-dialog.component.ts +++ b/src-ui/src/app/components/common/delete-dialog/delete-dialog.component.ts @@ -22,6 +22,21 @@ export class DeleteDialogComponent implements OnInit { @Input() message2 + deleteButtonEnabled = true + seconds = 0 + + delayConfirm(seconds: number) { + this.deleteButtonEnabled = false + this.seconds = seconds + setTimeout(() => { + if (this.seconds <= 1) { + this.deleteButtonEnabled = true + } else { + this.delayConfirm(seconds - 1) + } + }, 1000) + } + ngOnInit(): void { } From 56dfc71bb9f5c45b58eb338b9deeee8e6b413c4e Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Fri, 11 Dec 2020 14:48:33 +0100 Subject: [PATCH 0052/1708] document list service: selection model --- .../services/document-list-view.service.ts | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src-ui/src/app/services/document-list-view.service.ts b/src-ui/src/app/services/document-list-view.service.ts index 149096591..b3fe351ac 100644 --- a/src-ui/src/app/services/document-list-view.service.ts +++ b/src-ui/src/app/services/document-list-view.service.ts @@ -118,6 +118,7 @@ export class DocumentListViewService { //want changes in the filter editor to propagate into here right away. this.view.filterRules = cloneFilterRules(filterRules) this.reload() + this.reduceSelectionToFilter() this.saveDocumentListView() } @@ -192,6 +193,49 @@ export class DocumentListViewService { } } + selected = new Set<number>() + + selectNone() { + this.selected.clear() + } + + private reduceSelectionToFilter() { + if (this.selected.size > 0) { + this.documentService.listAllFilteredIds(this.filterRules).subscribe(ids => { + let subset = new Set<number>() + for (let id of ids) { + if (this.selected.has(id)) { + subset.add(id) + } + } + this.selected = subset + }) + } + } + + selectAll() { + this.documentService.listAllFilteredIds(this.filterRules).subscribe(ids => ids.forEach(id => this.selected.add(id))) + } + + selectPage() { + this.selected.clear() + this.documents.forEach(doc => { + this.selected.add(doc.id) + }) + } + + isSelected(d: PaperlessDocument) { + return this.selected.has(d.id) + } + + setSelected(d: PaperlessDocument, value: boolean) { + if (value) { + this.selected.add(d.id) + } else if (!value) { + this.selected.delete(d.id) + } + } + constructor(private documentService: DocumentService) { let documentListViewConfigJson = sessionStorage.getItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG) if (documentListViewConfigJson) { From d1f285113d2035aebc21a4f91d1e18889b7b78ad Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Fri, 11 Dec 2020 14:49:22 +0100 Subject: [PATCH 0053/1708] bulk edit menu and methods --- .../document-list.component.html | 34 +++--- .../document-list/document-list.component.ts | 112 +++++++++++++++++- 2 files changed, 129 insertions(+), 17 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index 58c32e9d1..24def7d64 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -7,22 +7,19 @@ </svg> Bulk edit </button> - <div ngbDropdownMenu aria-labelledby="dropdownBasic1"> - <button ngbDropdownItem>Select page</button> - <button ngbDropdownItem>Select all</button> - <button ngbDropdownItem>Select none</button> + <div ngbDropdownMenu aria-labelledby="dropdownBasic1" class="shadow"> + <button ngbDropdownItem (click)="list.selectPage()">Select page</button> + <button ngbDropdownItem (click)="list.selectAll()">Select all</button> + <button ngbDropdownItem (click)="list.selectNone()">Select none</button> <div class="dropdown-divider"></div> - <button ngbDropdownItem>Re-create archived document</button> + <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>Set correspondent</button> - <button ngbDropdownItem>Remove correspondent</button> - <button ngbDropdownItem>Set document type</button> - <button ngbDropdownItem>Remove document type</button> - <button ngbDropdownItem>Add tag</button> - <button ngbDropdownItem>Remove tag</button> - <div class="dropdown-divider"></div> - <button ngbDropdownItem>Delete</button> - + <button ngbDropdownItem [disabled]="list.selected.size == 0" (click)="bulkDelete()">Delete</button> </div> </div> @@ -101,7 +98,7 @@ </div> <div class="d-flex justify-content-between align-items-center"> - <p>{{list.collectionSize || 0}} document(s) <span *ngIf="isFiltered">(filtered)</span></p> + <p><span *ngIf="list.selected.size > 0">Selected {{list.selected.size}} of </span>{{list.collectionSize || 0}} document(s) <span *ngIf="isFiltered">(filtered)</span></p> <ngb-pagination [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5" [rotate]="true" (pageChange)="list.reload()" aria-label="Default pagination"></ngb-pagination> </div> @@ -113,6 +110,7 @@ <table class="table table-sm border shadow-sm" *ngIf="displayMode == 'details'"> <thead> + <th></th> <th class="d-none d-lg-table-cell">ASN</th> <th class="d-none d-md-table-cell">Correspondent</th> <th>Title</th> @@ -122,6 +120,12 @@ </thead> <tbody> <tr *ngFor="let d of list.documents"> + <td> + <div class="custom-control custom-checkbox"> + <input type="checkbox" class="custom-control-input" id="docCheck{{d.id}}" [checked]="list.isSelected(d)" (change)="list.setSelected(d, $event.target.checked)"> + <label class="custom-control-label" for="docCheck{{d.id}}"></label> + </div> + </td> <td class="d-none d-lg-table-cell"> {{d.archive_serial_number}} </td> diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index 09e73dd96..4d5597220 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -2,14 +2,21 @@ import { Component, OnInit } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; import { cloneFilterRules, FilterRule } from 'src/app/data/filter-rule'; import { FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; import { SavedViewConfig } from 'src/app/data/saved-view-config'; import { DocumentListViewService } from 'src/app/services/document-list-view.service'; -import { DOCUMENT_SORT_FIELDS } from 'src/app/services/rest/document.service'; +import { CorrespondentService } from 'src/app/services/rest/correspondent.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 { SavedViewConfigService } from 'src/app/services/saved-view-config.service'; import { Toast, ToastService } from 'src/app/services/toast.service'; import { environment } from 'src/environments/environment'; +import { DeleteDialogComponent } from '../common/delete-dialog/delete-dialog.component'; +import { SelectDialogComponent } from '../common/select-dialog/select-dialog.component'; import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component'; @Component({ @@ -25,7 +32,11 @@ export class DocumentListComponent implements OnInit { public route: ActivatedRoute, private toastService: ToastService, public modalService: NgbModal, - private titleService: Title) { } + private titleService: Title, + private correspondentService: CorrespondentService, + private documentTypeService: DocumentTypeService, + private tagService: TagService, + private documentService: DocumentService) { } displayMode = 'smallCards' // largeCards, smallCards, details @@ -142,4 +153,101 @@ export class DocumentListComponent implements OnInit { this.applyFilterRules() } + private executeBulkOperation(method: string, args): Observable<any> { + return this.documentService.bulkEdit(Array.from(this.list.selected), method, args).pipe( + map(r => { + + this.list.reload() + this.list.selectNone() + + return r + }) + ) + } + + 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() { + this.executeBulkOperation('set_correspondent', {"correspondent": null}).subscribe(r => {}) + } + + 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() { + this.executeBulkOperation('set_document_type', {"document_type": null}).subscribe(r => {}) + } + + 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(DeleteDialogComponent, {backdrop: 'static'}) + modal.componentInstance.delayConfirm(5) + modal.componentInstance.message = `This operation will permanently delete all ${this.list.selected.size} selected document(s).` + modal.componentInstance.message2 = `This operation cannot be undone.` + modal.componentInstance.deleteClicked.subscribe(() => { + this.executeBulkOperation("delete", {}).subscribe( + response => { + modal.close() + } + ) + }) + } } From 66240188c750d215870e9eda6ee0a4fda1cd064d Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Fri, 11 Dec 2020 14:51:20 +0100 Subject: [PATCH 0054/1708] import fix --- src/documents/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/documents/views.py b/src/documents/views.py index 4ce78348e..5e173d703 100755 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -31,7 +31,6 @@ from rest_framework.viewsets import ( import documents.index as index from paperless.db import GnuPG from paperless.views import StandardPagination -from .bulk_edit import perform_bulk_edit from .filters import ( CorrespondentFilterSet, DocumentFilterSet, From d1d09ac6acf8f8d539483c6af464188cdde322b8 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Fri, 11 Dec 2020 17:35:21 +0100 Subject: [PATCH 0055/1708] checboxes for small cards. does not work yet. --- .../document-card-small.component.html | 11 ++++++++++- .../document-card-small.component.scss | 8 ++++++++ .../document-card-small.component.ts | 2 ++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html index da469ebc4..8993674ba 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html @@ -1,7 +1,16 @@ -<div class="col p-2 h-100" style="width: 16rem;"> +<div class="col p-2 h-100 document-card" style="width: 16rem;"> <div class="card h-100 shadow-sm"> <div class="border-bottom"> <img class="card-img doc-img" [src]="getThumbUrl()"> + + <div style="top: 0; left: 0" class="position-absolute border-right border-bottom bg-light p-1" [class.document-card-check]="!selected"> + <div class="custom-control custom-checkbox"> + <input type="checkbox" class="custom-control-input" id="smallCardCheck{{document.id}}" [(ngModel)]="selected"> + <label class="custom-control-label" for="smallCardCheck{{document.id}}">L</label> + </div> + </div> + + <div style="top: 0; right: 0; font-size: large" class="text-right position-absolute mr-1"> <div *ngFor="let t of getTagsLimited$() | async"> <app-tag [tag]="t" (click)="clickTag.emit(t.id)" [clickable]="true" linkTitle="Filter by tag"></app-tag> diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.scss b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.scss index 0068667d0..ba7190615 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.scss +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.scss @@ -2,4 +2,12 @@ object-fit: cover; object-position: top; height: 200px; +} + +.document-card-check { + display: none +} + +.document-card:hover .document-card-check { + display: block; } \ No newline at end of file diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts index d60552d4f..037c02cf0 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts @@ -13,6 +13,8 @@ export class DocumentCardSmallComponent implements OnInit { constructor(private documentService: DocumentService) { } + selected = false + @Input() document: PaperlessDocument From 80b47fa287aaaf637feb31073849996cd54fca0a Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Fri, 11 Dec 2020 23:33:59 +0100 Subject: [PATCH 0056/1708] codestyle --- src/documents/bulk_edit.py | 17 ++++++++++------- src/documents/tasks.py | 4 ++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/documents/bulk_edit.py b/src/documents/bulk_edit.py index 1349f9d54..aa5b8ea3f 100644 --- a/src/documents/bulk_edit.py +++ b/src/documents/bulk_edit.py @@ -8,11 +8,12 @@ def set_correspondent(doc_ids, correspondent): if correspondent: correspondent = Correspondent.objects.get(id=correspondent) - qs = Document.objects.filter(Q(id__in=doc_ids) & ~Q(correspondent=correspondent)) + qs = Document.objects.filter( + Q(id__in=doc_ids) & ~Q(correspondent=correspondent)) affected_docs = [doc.id for doc in qs] qs.update(correspondent=correspondent) - async_task("documents.tasks.bulk_rename_files", affected_docs) + async_task("documents.tasks.bulk_rename_files", document_ids=affected_docs) return "OK" @@ -21,11 +22,12 @@ def set_document_type(doc_ids, document_type): if document_type: document_type = DocumentType.objects.get(id=document_type) - qs = Document.objects.filter(Q(id__in=doc_ids) & ~Q(document_type=document_type)) + qs = Document.objects.filter( + Q(id__in=doc_ids) & ~Q(document_type=document_type)) affected_docs = [doc.id for doc in qs] qs.update(document_type=document_type) - async_task("documents.tasks.bulk_rename_files", affected_docs) + async_task("documents.tasks.bulk_rename_files", document_ids=affected_docs) return "OK" @@ -38,10 +40,11 @@ def add_tag(doc_ids, tag): DocumentTagRelationship = Document.tags.through DocumentTagRelationship.objects.bulk_create([ - DocumentTagRelationship(document_id=doc, tag_id=tag) for doc in affected_docs + DocumentTagRelationship( + document_id=doc, tag_id=tag) for doc in affected_docs ]) - async_task("documents.tasks.bulk_rename_files", affected_docs) + async_task("documents.tasks.bulk_rename_files", document_ids=affected_docs) return "OK" @@ -58,7 +61,7 @@ def remove_tag(doc_ids, tag): Q(tag_id=tag) ).delete() - async_task("documents.tasks.bulk_rename_files", affected_docs) + async_task("documents.tasks.bulk_rename_files", document_ids=affected_docs) return "OK" diff --git a/src/documents/tasks.py b/src/documents/tasks.py index af4c91448..fafe6e10f 100644 --- a/src/documents/tasks.py +++ b/src/documents/tasks.py @@ -90,7 +90,7 @@ def sanity_check(): return "No issues detected." -def bulk_rename_files(ids): - qs = Document.objects.filter(id__in=ids) +def bulk_rename_files(document_ids): + qs = Document.objects.filter(id__in=document_ids) for doc in qs: post_save.send(Document, instance=doc, created=False) From f5df9108945cdcfe0c095bcb64903b9269adad2f Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Fri, 11 Dec 2020 23:34:24 +0100 Subject: [PATCH 0057/1708] document list validation. --- src/documents/serialisers.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 5418ec0fb..92fc35719 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -185,6 +185,13 @@ class BulkEditSerializer(serializers.Serializer): parameters = serializers.DictField(allow_empty=True) + def validate_documents(self, documents): + count = Document.objects.filter(id__in=documents).count() + if not count == len(documents): + raise serializers.ValidationError( + "Some documents don't exist or were specified twice.") + return documents + def validate_method(self, method): if method == "set_correspondent": return bulk_edit.set_correspondent From a85792e327ffb5604f40c69d72cfce47cfa2b623 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Fri, 11 Dec 2020 23:34:34 +0100 Subject: [PATCH 0058/1708] tests. --- src/documents/tests/test_api.py | 116 +++++++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 1 deletion(-) diff --git a/src/documents/tests/test_api.py b/src/documents/tests/test_api.py index ab1716366..bd0d9a421 100644 --- a/src/documents/tests/test_api.py +++ b/src/documents/tests/test_api.py @@ -1,14 +1,16 @@ +import json import os import shutil import tempfile from unittest import mock from django.contrib.auth.models import User +from django.test import client from pathvalidate import ValidationError from rest_framework.test import APITestCase from whoosh.writing import AsyncWriter -from documents import index +from documents import index, bulk_edit from documents.models import Document, Correspondent, DocumentType, Tag from documents.tests.utils import DirectoriesMixin @@ -515,3 +517,115 @@ class TestDocumentApi(DirectoriesMixin, APITestCase): self.assertFalse(meta['has_archive_version']) self.assertGreater(len(meta['original_metadata']), 0) self.assertIsNone(meta['archive_metadata']) + + +class TestBulkEdit(DirectoriesMixin, APITestCase): + + def setUp(self): + super(TestBulkEdit, self).setUp() + + user = User.objects.create_superuser(username="temp_admin") + self.client.force_login(user=user) + + patcher = mock.patch('documents.bulk_edit.async_task') + self.async_task = patcher.start() + self.addCleanup(patcher.stop) + self.c1 = Correspondent.objects.create(name="c1") + self.c2 = Correspondent.objects.create(name="c2") + self.dt1 = DocumentType.objects.create(name="dt1") + self.dt2 = DocumentType.objects.create(name="dt2") + self.t1 = Tag.objects.create(name="t1") + self.t2 = Tag.objects.create(name="t2") + self.doc1 = Document.objects.create(checksum="A", title="A") + self.doc2 = Document.objects.create(checksum="B", title="B", correspondent=self.c1, document_type=self.dt1) + self.doc3 = Document.objects.create(checksum="C", title="C", correspondent=self.c2, document_type=self.dt2) + self.doc4 = Document.objects.create(checksum="D", title="D") + self.doc5 = Document.objects.create(checksum="E", title="E") + self.doc2.tags.add(self.t1) + self.doc3.tags.add(self.t2) + self.doc4.tags.add(self.t1, self.t2) + + def test_set_correspondent(self): + self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 1) + bulk_edit.set_correspondent([self.doc1.id, self.doc2.id, self.doc3.id], self.c2.id) + self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 3) + self.async_task.assert_called_once() + args, kwargs = self.async_task.call_args + self.assertCountEqual(kwargs['document_ids'], [self.doc1.id, self.doc2.id]) + + def test_unset_correspondent(self): + self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 1) + bulk_edit.set_correspondent([self.doc1.id, self.doc2.id, self.doc3.id], None) + self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 0) + self.async_task.assert_called_once() + args, kwargs = self.async_task.call_args + self.assertCountEqual(kwargs['document_ids'], [self.doc2.id, self.doc3.id]) + + def test_set_document_type(self): + self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 1) + bulk_edit.set_document_type([self.doc1.id, self.doc2.id, self.doc3.id], self.dt2.id) + self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 3) + self.async_task.assert_called_once() + args, kwargs = self.async_task.call_args + self.assertCountEqual(kwargs['document_ids'], [self.doc1.id, self.doc2.id]) + + def test_unset_document_type(self): + self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 1) + bulk_edit.set_document_type([self.doc1.id, self.doc2.id, self.doc3.id], None) + self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 0) + self.async_task.assert_called_once() + args, kwargs = self.async_task.call_args + self.assertCountEqual(kwargs['document_ids'], [self.doc2.id, self.doc3.id]) + + def test_add_tag(self): + self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 2) + bulk_edit.add_tag([self.doc1.id, self.doc2.id, self.doc3.id, self.doc4.id], self.t1.id) + self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 4) + self.async_task.assert_called_once() + args, kwargs = self.async_task.call_args + self.assertCountEqual(kwargs['document_ids'], [self.doc1.id, self.doc3.id]) + + + def test_remove_tag(self): + self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 2) + bulk_edit.remove_tag([self.doc1.id, self.doc3.id, self.doc4.id], self.t1.id) + self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 1) + self.async_task.assert_called_once() + args, kwargs = self.async_task.call_args + self.assertCountEqual(kwargs['document_ids'], [self.doc4.id]) + + def test_delete(self): + self.assertEqual(Document.objects.count(), 5) + bulk_edit.delete([self.doc1.id, self.doc2.id]) + self.assertEqual(Document.objects.count(), 3) + self.assertCountEqual([doc.id for doc in Document.objects.all()], [self.doc3.id, self.doc4.id, self.doc5.id]) + + def test_api(self): + self.assertEqual(Document.objects.count(), 5) + response = self.client.post("/api/documents/bulk_edit/", json.dumps({ + "documents": [self.doc1.id], + "method": "delete", + "parameters": {} + }), content_type='application/json') + self.assertEqual(response.status_code, 200) + self.assertEqual(Document.objects.count(), 4) + + def test_api_invalid_doc(self): + self.assertEqual(Document.objects.count(), 5) + response = self.client.post("/api/documents/bulk_edit/", json.dumps({ + "documents": [-235], + "method": "delete", + "parameters": {} + }), content_type='application/json') + self.assertEqual(response.status_code, 400) + self.assertEqual(Document.objects.count(), 5) + + def test_api_invalid_method(self): + self.assertEqual(Document.objects.count(), 5) + response = self.client.post("/api/documents/bulk_edit/", json.dumps({ + "documents": [self.doc2.id], + "method": "exterminate", + "parameters": {} + }), content_type='application/json') + self.assertEqual(response.status_code, 400) + self.assertEqual(Document.objects.count(), 5) From e7cb35853615791b7f18aeff770b8fcb34abe43e Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Fri, 11 Dec 2020 15:20:47 -0800 Subject: [PATCH 0059/1708] Fix broken card tags / correspondent links --- .../document-card-large.component.html | 4 ++-- .../document-card-small.component.html | 8 ++++---- .../document-list.component.html | 12 +++++------ .../document-list/document-list.component.ts | 20 ++++++++++++++++++- .../filter-editor/filter-editor.component.ts | 9 ++++++--- 5 files changed, 37 insertions(+), 16 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html index bfc59b526..430a76a23 100644 --- a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html +++ b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html @@ -7,7 +7,7 @@ <div class="card-body"> <div class="d-flex justify-content-between align-items-center"> - <h5 class="card-title"> + <h5 class="card-title"> <ng-container *ngIf="document.correspondent"> <a *ngIf="clickCorrespondent.observers.length ; else nolink" [routerLink]="" title="Filter by correspondent" (click)="clickCorrespondent.emit(document.correspondent)" class="font-weight-bold">{{(document.correspondent$ | async)?.name}}</a> <ng-template #nolink>{{(document.correspondent$ | async)?.name}}</ng-template>: @@ -52,4 +52,4 @@ </div> </div> </div> -</div> \ No newline at end of file +</div> diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html index 95cf2e191..b2e7b0218 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html @@ -11,7 +11,7 @@ </div> </div> </div> - + <div class="card-body p-2"> <p class="card-text"> <ng-container *ngIf="document.correspondent"> @@ -44,7 +44,7 @@ </div> <small class="text-muted">{{document.created | date}}</small> </div> - + </div> - </div> -</div> \ No newline at end of file + </div> +</div> diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index f8c3445c5..a6ec1b741 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -64,7 +64,7 @@ <div class="card w-100 mb-3"> <div class="card-body"> - <app-filter-editor [(filterRules)]="filterRules" (apply)="applyFilterRules()" (clear)="clearFilterRules()"></app-filter-editor> + <app-filter-editor [(filterRules)]="filterRules" (apply)="applyFilterRules()" (clear)="clearFilterRules()" #filterEditor></app-filter-editor> </div> </div> @@ -75,7 +75,7 @@ </div> <div *ngIf="displayMode == 'largeCards'"> - <app-document-card-large *ngFor="let d of list.documents" [document]="d" [details]="d.content" (clickTag)="filterByTag($event)" (clickCorrespondent)="filterByCorrespondent($event)"> + <app-document-card-large *ngFor="let d of list.documents" [document]="d" [details]="d.content" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)"> </app-document-card-large> </div> @@ -95,16 +95,16 @@ </td> <td class="d-none d-md-table-cell"> <ng-container *ngIf="d.correspondent"> - <a [routerLink]="" (click)="filterByCorrespondent(d.correspondent)" title="Filter by correspondent">{{(d.correspondent$ | async)?.name}}</a> + <a [routerLink]="" (click)="clickCorrespondent(d.correspondent)" title="Filter by correspondent">{{(d.correspondent$ | async)?.name}}</a> </ng-container> </td> <td> <a routerLink="/documents/{{d.id}}" title="Edit document" style="overflow-wrap: anywhere;">{{d.title}}</a> - <app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ml-1" clickable="true" linkTitle="Filter by tag" (click)="filterByTag(t.id)"></app-tag> + <app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ml-1" clickable="true" linkTitle="Filter by tag" (click)="clickTag(t)"></app-tag> </td> <td class="d-none d-xl-table-cell"> <ng-container *ngIf="d.document_type"> - <a [routerLink]="" (click)="filterByDocumentType(d.document_type)" title="Filter by document type">{{(d.document_type$ | async)?.name}}</a> + <a [routerLink]="" (click)="clickDocumentType(d.document_type)" title="Filter by document type">{{(d.document_type$ | async)?.name}}</a> </ng-container> </td> <td> @@ -119,5 +119,5 @@ <div class=" m-n2 row" *ngIf="displayMode == 'smallCards'"> - <app-document-card-small [document]="d" *ngFor="let d of list.documents" (clickTag)="filterByTag($event)" (clickCorrespondent)="filterByCorrespondent($event)"></app-document-card-small> + <app-document-card-small [document]="d" *ngFor="let d of list.documents" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)"></app-document-card-small> </div> diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index 3ce00beab..5bbd5b49d 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, ViewChild } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; @@ -11,6 +11,10 @@ import { SavedViewConfigService } from 'src/app/services/saved-view-config.servi import { Toast, ToastService } from 'src/app/services/toast.service'; import { environment } from 'src/environments/environment'; import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component'; +import { FilterEditorComponent } from './filter-editor/filter-editor.component'; +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-document-list', @@ -31,6 +35,8 @@ export class DocumentListComponent implements OnInit { filterRules: FilterRule[] = [] + @ViewChild('filterEditor') filterEditor: FilterEditorComponent + get isFiltered() { return this.list.filterRules?.length > 0 } @@ -99,4 +105,16 @@ export class DocumentListComponent implements OnInit { }) } + clickTag(tagID: number) { + this.filterEditor.toggleFilterByItem(tagID, FILTER_HAS_TAG) + } + + clickCorrespondent(correspondentID: number) { + this.filterEditor.toggleFilterByItem(correspondentID, FILTER_CORRESPONDENT) + } + + clickDocumentType(documentTypeID: number) { + this.filterEditor.toggleFilterByItem(documentTypeID, FILTER_DOCUMENT_TYPE) + } + } diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/filter-editor/filter-editor.component.ts index 93a91473f..b73b4387b 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.ts @@ -1,9 +1,9 @@ import { Component, EventEmitter, Input, OnInit, Output, ElementRef, AfterViewInit, QueryList, ViewChild, ViewChildren } from '@angular/core'; import { FilterRule } from 'src/app/data/filter-rule'; import { FilterRuleType, FILTER_RULE_TYPES, FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_TITLE, FILTER_ADDED_BEFORE, FILTER_ADDED_AFTER, FILTER_CREATED_BEFORE, FILTER_CREATED_AFTER, FILTER_CREATED_YEAR, FILTER_CREATED_MONTH, FILTER_CREATED_DAY } from 'src/app/data/filter-rule-type'; +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 { PaperlessTag } from 'src/app/data/paperless-tag'; import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'; import { ObjectWithId } from 'src/app/data/object-with-id'; import { CorrespondentService } from 'src/app/services/rest/correspondent.service'; @@ -120,7 +120,11 @@ export class FilterEditorComponent implements OnInit, AfterViewInit { this.applySelected() } - toggleFilterByItem(item: ObjectWithId, filterRuleTypeID: number) { + toggleFilterByItem(item: any, filterRuleTypeID: number) { + let dropdown = this.getDropdownByFilterRuleTypeID(filterRuleTypeID) + if (typeof item == 'number') { + item = dropdown.items.find(i => i.id == item) + } let filterRules = this.filterRules let filterRuleType: FilterRuleType = FILTER_RULE_TYPES.find(t => t.id == filterRuleTypeID) let existingRule = filterRules.find(rule => rule.type.id == filterRuleType.id) @@ -137,7 +141,6 @@ export class FilterEditorComponent implements OnInit, AfterViewInit { filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == filterRuleType.id), value: item.id}) } - let dropdown = this.getDropdownByFilterRuleTypeID(filterRuleTypeID) this.updateDropdownActiveItems(dropdown) this.filterRules = filterRules From 9cd40e96f41100cf0056c5f2b7e912cf381ddc7f Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sat, 12 Dec 2020 01:09:52 -0800 Subject: [PATCH 0060/1708] Working date filtering --- .../document-list/document-list.component.ts | 2 +- .../filter-dropdown-date.component.html | 12 ++++---- .../filter-dropdown-date.component.ts | 30 ++++++++++++------- .../filter-editor.component.html | 2 +- .../filter-editor/filter-editor.component.ts | 17 ++++++++++- 5 files changed, 43 insertions(+), 20 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index 5bbd5b49d..271f6f7e5 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -11,7 +11,7 @@ import { SavedViewConfigService } from 'src/app/services/saved-view-config.servi import { Toast, ToastService } from 'src/app/services/toast.service'; import { environment } from 'src/environments/environment'; import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component'; -import { FilterEditorComponent } from './filter-editor/filter-editor.component'; +import { FilterEditorComponent } from 'src/app/components/filter-editor/filter-editor.component'; 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'; diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.html index 74d508390..fb514d7df 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.html @@ -3,15 +3,15 @@ <div class="dropdown-menu date-filter shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> <div class="list-group list-group-flush"> <div class="list-group-item d-flex flex-column align-items-start"> - <button class="btn btn-sm btn-link pl-0" (click)="setQuickFilter(7)">Last 7 days</button> - <button class="btn btn-sm btn-link pl-0" (click)="setQuickFilter(30)">Last 30 days</button> - <button class="btn btn-sm btn-link pl-0" *ngIf="showMonth" (click)="setQuickFilter('month')">This month</button> - <button class="btn btn-sm btn-link pl-0" *ngIf="showYear" (click)="setQuickFilter('year')">This year</button> + <button class="btn btn-sm btn-link pl-0" (click)="setDateQuickFilter(7)">Last 7 days</button> + <button class="btn btn-sm btn-link pl-0" (click)="setDateQuickFilter(30)">Last 30 days</button> + <button class="btn btn-sm btn-link pl-0" (click)="setDateQuickFilter('month')">This month</button> + <button class="btn btn-sm btn-link pl-0" (click)="setDateQuickFilter('year')">This year</button> </div> <div class="list-group-item d-flex flex-column align-items-start" role="menuitem"> <div class="mb-1"><small>Before</small></div> <div class="input-group input-group-sm"> - <input class="form-control" type="text" placeholder="yyyy-mm-dd" name="before" [(ngModel)]="dateBefore" ngbDatepicker #dpBefore="ngbDatepicker"> + <input class="form-control" type="text" placeholder="yyyy-mm-dd" name="before" [(ngModel)]="dateBefore" ngbDatepicker (dateSelect)="dateSelected($event)" #dpBefore="ngbDatepicker"> <div class="input-group-append"> <button class="btn btn-outline-secondary btn-sm" (click)="dpBefore.toggle()" type="button"> <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-calendar-date" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> @@ -25,7 +25,7 @@ <div class="list-group-item d-flex flex-column align-items-start" role="menuitem"> <div class="mb-1"><small>After</small></div> <div class="input-group"> - <input class="form-control form-control-sm" type="text" placeholder="yyyy-mm-dd" name="after" [(ngModel)]="dateAfter" ngbDatepicker #dpAfter="ngbDatepicker"> + <input class="form-control form-control-sm" type="text" placeholder="yyyy-mm-dd" name="after" [(ngModel)]="dateAfter" ngbDatepicker (dateSelect)="dateSelected($event)" #dpAfter="ngbDatepicker"> <div class="input-group-append"> <button class="btn btn-outline-secondary btn-sm" (click)="dpAfter.toggle()" type="button"> <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-calendar-date" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.ts index 37ea2cd09..9044f34a9 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.ts @@ -1,4 +1,5 @@ import { Component, EventEmitter, Input, OnInit, Output, ElementRef, ViewChild } from '@angular/core'; +import { FilterRule } from 'src/app/data/filter-rule'; import { FilterRuleType, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; import { ObjectWithId } from 'src/app/data/object-with-id'; import { FilterDropdownComponent } from '../filter-dropdown.component' @@ -18,8 +19,6 @@ export class FilterDropdownDateComponent extends FilterDropdownComponent { selected = new EventEmitter() filterRuleTypes: FilterRuleType[] = [] - showYear: boolean = false - showMonth: boolean = false dateAfter: NgbDateStruct dateBefore: NgbDateStruct @@ -27,27 +26,36 @@ export class FilterDropdownDateComponent extends FilterDropdownComponent { this.filterRuleTypes = this.filterRuleTypeIDs.map(id => FILTER_RULE_TYPES.find(rt => rt.id == id)) this.filterRuleTypeID = this.filterRuleTypeIDs[0] super.ngOnInit() - - this.showYear = this.filterRuleTypes.find(rt => rt.filtervar.indexOf('year') > -1) !== undefined - this.showMonth = this.filterRuleTypes.find(rt => rt.filtervar.indexOf('month') > -1) !== undefined } - setQuickFilter(range: any) { + setDateQuickFilter(range: any) { this.dateAfter = this.dateBefore = undefined + let now = new Date() switch (typeof range) { case 'number': - let date = new Date(); - date.setDate(date.getDate() - range) - this.dateAfter = { year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate() } + now.setDate(now.getDate() - range) + this.dateAfter = { year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate() } + this.dateSelected(this.dateAfter) break; case 'string': - let filterRuleType = this.filterRuleTypes.find(rt => rt.filtervar.indexOf(range) > -1) - console.log(range); + let date = { year: now.getFullYear(), month: now.getMonth() + 1, day: 1 } + if (range == 'year') date.month = 1 + this.dateAfter = date + this.dateSelected(this.dateAfter) break; default: break; } } + + dateSelected(date:NgbDateStruct) { + let isAfter = this.dateAfter !== undefined + let filterRuleType = this.filterRuleTypes.find(rt => rt.filtervar.indexOf(isAfter ? 'gt' : 'lt') > -1) + if (filterRuleType) { + let dateFilterRule:FilterRule = {value: `${date.year}-${date.month}-${date.day}`, type: filterRuleType} + this.selected.emit(dateFilterRule) + } + } } diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.html b/src-ui/src/app/components/filter-editor/filter-editor.component.html index d3473337b..368eed564 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.html +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.html @@ -8,7 +8,7 @@ <app-filter-dropdown class="col-auto" *ngFor="let quickFilterRuleTypeID of quickFilterRuleTypeIDs" [filterRuleTypeID]="quickFilterRuleTypeID" (toggle)="toggleFilterByItem($event, quickFilterRuleTypeID)"></app-filter-dropdown> - <app-filter-dropdown-date class="col-auto" *ngFor="let dateAddedFilterRuleTypeID of dateAddedFilterRuleTypeIDs" [filterRuleTypeIDs]="dateAddedFilterRuleTypeID" (toggle)="toggleFilterByItem($event, quickFilterRuleTypeID)"></app-filter-dropdown-date> + <app-filter-dropdown-date class="col-auto" *ngFor="let dateAddedFilterRuleTypeID of dateAddedFilterRuleTypeIDs" [filterRuleTypeIDs]="dateAddedFilterRuleTypeID" (selected)="setDateFilter($event)"></app-filter-dropdown-date> <button class="btn btn-link btn-sm" [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"> diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/filter-editor/filter-editor.component.ts index b73b4387b..4ac7769d5 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.ts @@ -12,6 +12,7 @@ import { TagService } from 'src/app/services/rest/tag.service'; import { FilterDropdownComponent } from './filter-dropdown/filter-dropdown.component' import { fromEvent } from 'rxjs'; import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; +import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'app-filter-editor', @@ -35,7 +36,7 @@ export class FilterEditorComponent implements OnInit, AfterViewInit { @ViewChildren(FilterDropdownComponent) quickFilterDropdowns!: QueryList<FilterDropdownComponent>; quickFilterRuleTypeIDs: number[] = [FILTER_HAS_TAG, FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE] - dateAddedFilterRuleTypeIDs: any[] = [[FILTER_ADDED_BEFORE, FILTER_ADDED_AFTER], [FILTER_CREATED_BEFORE, FILTER_CREATED_AFTER, FILTER_CREATED_YEAR, FILTER_CREATED_MONTH, FILTER_CREATED_DAY]] + dateAddedFilterRuleTypeIDs: any[] = [[FILTER_ADDED_BEFORE, FILTER_ADDED_AFTER], [FILTER_CREATED_BEFORE, FILTER_CREATED_AFTER]] correspondents: PaperlessCorrespondent[] = [] tags: PaperlessTag[] = [] @@ -147,4 +148,18 @@ export class FilterEditorComponent implements OnInit, AfterViewInit { this.applySelected() } + setDateFilter(newFilterRule: FilterRule) { + let filterRules = this.filterRules + let existingRule = filterRules.find(rule => rule.type.id == newFilterRule.type.id) + + if (existingRule) { + existingRule.value = newFilterRule.value + } else { + filterRules.push(newFilterRule) + } + + this.filterRules = filterRules + this.applySelected() + } + } From 02871e1e22440fba9b6de578abab609a85ef0bce Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sat, 12 Dec 2020 02:07:25 -0800 Subject: [PATCH 0061/1708] Date filter clearing --- .../filter-dropdown-date.component.ts | 28 ++++++++------- .../filter-editor/filter-editor.component.ts | 35 +++++++++++++------ 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.ts index 9044f34a9..baadcc4e6 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.ts @@ -30,31 +30,33 @@ export class FilterDropdownDateComponent extends FilterDropdownComponent { setDateQuickFilter(range: any) { this.dateAfter = this.dateBefore = undefined - let now = new Date() + let date = new Date() + let newDate: NgbDateStruct = { year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate() } switch (typeof range) { case 'number': - now.setDate(now.getDate() - range) - this.dateAfter = { year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate() } - this.dateSelected(this.dateAfter) - break; + date.setDate(date.getDate() - range) + newDate.year = date.getFullYear() + newDate.month = date.getMonth() + 1 + newDate.day = date.getDate() + break case 'string': - let date = { year: now.getFullYear(), month: now.getMonth() + 1, day: 1 } - if (range == 'year') date.month = 1 - this.dateAfter = date - this.dateSelected(this.dateAfter) - break; + newDate.day = 1 + if (range == 'year') newDate.month = 1 + break default: - break; + break } + this.dateAfter = newDate + this.dateSelected(this.dateAfter) } dateSelected(date:NgbDateStruct) { - let isAfter = this.dateAfter !== undefined + let isAfter = this.dateAfter == date let filterRuleType = this.filterRuleTypes.find(rt => rt.filtervar.indexOf(isAfter ? 'gt' : 'lt') > -1) if (filterRuleType) { - let dateFilterRule:FilterRule = {value: `${date.year}-${date.month}-${date.day}`, type: filterRuleType} + let dateFilterRule:FilterRule = {value: `${date.year}-${date.month.toString().padStart(2,0)}-${date.day.toString().padStart(2,0)}`, type: filterRuleType} this.selected.emit(dateFilterRule) } } diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/filter-editor/filter-editor.component.ts index 4ac7769d5..05dd3a92a 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.ts @@ -10,6 +10,7 @@ import { CorrespondentService } from 'src/app/services/rest/correspondent.servic import { DocumentTypeService } from 'src/app/services/rest/document-type.service'; import { TagService } from 'src/app/services/rest/tag.service'; import { FilterDropdownComponent } from './filter-dropdown/filter-dropdown.component' +import { FilterDropdownDateComponent } from './filter-dropdown/filter-dropdown-date/filter-dropdown-date.component' import { fromEvent } from 'rxjs'; import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; @@ -34,6 +35,7 @@ export class FilterEditorComponent implements OnInit, AfterViewInit { @ViewChild('filterTextInput') filterTextInput: ElementRef; @ViewChildren(FilterDropdownComponent) quickFilterDropdowns!: QueryList<FilterDropdownComponent>; + @ViewChildren(FilterDropdownDateComponent) quickDateFilterDropdowns!: QueryList<FilterDropdownDateComponent>; quickFilterRuleTypeIDs: number[] = [FILTER_HAS_TAG, FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE] dateAddedFilterRuleTypeIDs: any[] = [[FILTER_ADDED_BEFORE, FILTER_ADDED_AFTER], [FILTER_CREATED_BEFORE, FILTER_CREATED_AFTER]] @@ -52,16 +54,15 @@ export class FilterEditorComponent implements OnInit, AfterViewInit { } ngAfterViewInit() { - fromEvent(this.filterTextInput.nativeElement,'keyup') - .pipe( - debounceTime(150), - distinctUntilChanged(), - tap() - ) - .subscribe((event: Event) => { - this.filterText = (event.target as HTMLInputElement).value - this.onTextFilterInput() - }); + fromEvent(this.filterTextInput.nativeElement,'keyup').pipe( + debounceTime(150), + distinctUntilChanged(), + tap() + ).subscribe((event: Event) => { + this.filterText = (event.target as HTMLInputElement).value + this.onTextFilterInput() + }) + this.quickDateFilterDropdowns.forEach(d => this.updateDateDropdown(d)) } setDropdownItems(items: ObjectWithId[], filterRuleTypeID: number): void { @@ -69,7 +70,6 @@ export class FilterEditorComponent implements OnInit, AfterViewInit { if (dropdown) { dropdown.items = items } - this.updateDropdownActiveItems(dropdown) } updateDropdownActiveItems(dropdown: FilterDropdownComponent): void { @@ -81,6 +81,18 @@ export class FilterEditorComponent implements OnInit, AfterViewInit { dropdown.itemsActive = activeItems } + updateDateDropdown(dateDropdown: FilterDropdownDateComponent) { + let activeRules = this.filterRules.filter(r => dateDropdown.filterRuleTypeIDs.includes(r.type.id)) + if (activeRules.length > 0) { + activeRules.forEach(rule => { + let date = { year: rule.value.substring(0,4), month: rule.value.substring(5,7), day: rule.value.substring(8,10) } + rule.type.filtervar.indexOf('gt') > -1 ? dateDropdown.dateAfter = date : dateDropdown.dateBefore = date + }) + } else { + dateDropdown.dateAfter = dateDropdown.dateBefore = undefined + } + } + getDropdownByFilterRuleTypeID(filterRuleTypeID: number): FilterDropdownComponent { return this.quickFilterDropdowns.find(d => d.filterRuleTypeID == filterRuleTypeID) } @@ -93,6 +105,7 @@ export class FilterEditorComponent implements OnInit, AfterViewInit { this.filterRules.splice(0,this.filterRules.length) this.updateTextFilterInput() this.quickFilterDropdowns.forEach(d => this.updateDropdownActiveItems(d)) + this.quickDateFilterDropdowns.forEach(d => this.updateDateDropdown(d)) this.clear.next() } From 2a57f9d6e578dcaa99e55f0e041aa07608600c14 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sat, 12 Dec 2020 02:14:15 -0800 Subject: [PATCH 0062/1708] NgbDate comparison error --- .../filter-dropdown-date/filter-dropdown-date.component.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.ts index baadcc4e6..2973a25eb 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.ts @@ -3,7 +3,7 @@ import { FilterRule } from 'src/app/data/filter-rule'; import { FilterRuleType, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; import { ObjectWithId } from 'src/app/data/object-with-id'; import { FilterDropdownComponent } from '../filter-dropdown.component' -import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; +import { NgbDate, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'app-filter-dropdown-date', @@ -53,10 +53,11 @@ export class FilterDropdownDateComponent extends FilterDropdownComponent { } dateSelected(date:NgbDateStruct) { - let isAfter = this.dateAfter == date + let isAfter = NgbDate.from(this.dateAfter).equals(date) + let filterRuleType = this.filterRuleTypes.find(rt => rt.filtervar.indexOf(isAfter ? 'gt' : 'lt') > -1) if (filterRuleType) { - let dateFilterRule:FilterRule = {value: `${date.year}-${date.month.toString().padStart(2,0)}-${date.day.toString().padStart(2,0)}`, type: filterRuleType} + let dateFilterRule:FilterRule = {value: `${date.year}-${date.month.toString().padStart(2,'0')}-${date.day.toString().padStart(2,'0')}`, type: filterRuleType} this.selected.emit(dateFilterRule) } } From dfa1f29809efc9a395e7e379ad6e2ba82983cc1f Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Sat, 12 Dec 2020 15:46:56 +0100 Subject: [PATCH 0063/1708] add backend support for saved views --- src/documents/admin.py | 17 +++++++- .../1007_savedview_savedviewfilterrule.py | 37 ++++++++++++++++ src/documents/models.py | 42 +++++++++++++++++++ src/documents/serialisers.py | 36 +++++++++++++++- src/documents/views.py | 21 +++++++++- src/paperless/urls.py | 4 +- 6 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 src/documents/migrations/1007_savedview_savedviewfilterrule.py diff --git a/src/documents/admin.py b/src/documents/admin.py index 055a6fd93..6ec3b736e 100755 --- a/src/documents/admin.py +++ b/src/documents/admin.py @@ -4,7 +4,8 @@ from django.utils.safestring import mark_safe from whoosh.writing import AsyncWriter from . import index -from .models import Correspondent, Document, DocumentType, Log, Tag +from .models import Correspondent, Document, DocumentType, Log, Tag, \ + SavedView, SavedViewFilterRule class CorrespondentAdmin(admin.ModelAdmin): @@ -131,8 +132,22 @@ class LogAdmin(admin.ModelAdmin): list_display_links = ("created", "message") +class RuleInline(admin.TabularInline): + model = SavedViewFilterRule + + +class SavedViewAdmin(admin.ModelAdmin): + + list_display = ("name", "user") + + inlines = [ + RuleInline + ] + + admin.site.register(Correspondent, CorrespondentAdmin) admin.site.register(Tag, TagAdmin) admin.site.register(DocumentType, DocumentTypeAdmin) admin.site.register(Document, DocumentAdmin) admin.site.register(Log, LogAdmin) +admin.site.register(SavedView, SavedViewAdmin) diff --git a/src/documents/migrations/1007_savedview_savedviewfilterrule.py b/src/documents/migrations/1007_savedview_savedviewfilterrule.py new file mode 100644 index 000000000..664def5f1 --- /dev/null +++ b/src/documents/migrations/1007_savedview_savedviewfilterrule.py @@ -0,0 +1,37 @@ +# Generated by Django 3.1.4 on 2020-12-12 14:41 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('documents', '1006_auto_20201208_2209'), + ] + + operations = [ + migrations.CreateModel( + name='SavedView', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=128)), + ('show_on_dashboard', models.BooleanField()), + ('show_in_sidebar', models.BooleanField()), + ('sort_field', models.CharField(max_length=128)), + ('sort_reverse', models.BooleanField(default=False)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='SavedViewFilterRule', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('rule_type', models.PositiveIntegerField(choices=[(0, 'Title contains'), (1, 'Content contains'), (2, 'ASN is'), (3, 'Correspondent is'), (4, 'Document type is'), (5, 'Is in inbox'), (6, 'Has tag'), (7, 'Has any tag'), (8, 'Created before'), (9, 'Created after'), (10, 'Created year is'), (11, 'Created month is'), (12, 'Created day is'), (13, 'Added before'), (14, 'Added after'), (15, 'Modified before'), (16, 'Modified after'), (17, 'Does not have tag')])), + ('value', models.CharField(max_length=128)), + ('saved_view', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='filter_rules', to='documents.savedview')), + ], + ), + ] diff --git a/src/documents/models.py b/src/documents/models.py index f0678a843..1b1f697bc 100755 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -9,6 +9,7 @@ import pathvalidate import dateutil.parser from django.conf import settings +from django.contrib.auth.models import User from django.db import models from django.utils import timezone from django.utils.text import slugify @@ -305,6 +306,47 @@ class Log(models.Model): return self.message +class SavedView(models.Model): + + user = models.ForeignKey(User, on_delete=models.CASCADE) + name = models.CharField(max_length=128) + + show_on_dashboard = models.BooleanField() + show_in_sidebar = models.BooleanField() + + sort_field = models.CharField(max_length=128) + sort_reverse = models.BooleanField(default=False) + + +class SavedViewFilterRule(models.Model): + RULE_TYPES = [ + (0, "Title contains"), + (1, "Content contains"), + (2, "ASN is"), + (3, "Correspondent is"), + (4, "Document type is"), + (5, "Is in inbox"), + (6, "Has tag"), + (7, "Has any tag"), + (8, "Created before"), + (9, "Created after"), + (10, "Created year is"), + (11, "Created month is"), + (12, "Created day is"), + (13, "Added before"), + (14, "Added after"), + (15, "Modified before"), + (16, "Modified after"), + (17, "Does not have tag"), + ] + + saved_view = models.ForeignKey(SavedView, on_delete=models.CASCADE, related_name="filter_rules") + + rule_type = models.PositiveIntegerField(choices=RULE_TYPES) + + value = models.CharField(max_length=128) + + # TODO: why is this in the models file? class FileInfo: diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index db0e610d1..43b5e5992 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -3,7 +3,8 @@ from django.utils.text import slugify from rest_framework import serializers from rest_framework.fields import SerializerMethodField -from .models import Correspondent, Tag, Document, Log, DocumentType +from .models import Correspondent, Tag, Document, Log, DocumentType, \ + SavedView, SavedViewFilterRule from .parsers import is_mime_type_supported @@ -140,6 +141,39 @@ class LogSerializer(serializers.ModelSerializer): ) +class SavedViewFilterRuleSerializer(serializers.ModelSerializer): + + class Meta: + model = SavedViewFilterRule + fields = ["rule_type", "value"] + + +class SavedViewSerializer(serializers.ModelSerializer): + + filter_rules = SavedViewFilterRuleSerializer(many=True) + + class Meta: + model = SavedView + depth = 1 + fields = ["id", "name", "show_on_dashboard", "show_in_sidebar", + "sort_field", "sort_reverse", "filter_rules"] + + def update(self, instance, validated_data): + rules_data = validated_data.pop('filter_rules') + super(SavedViewSerializer, self).update(instance, validated_data) + SavedViewFilterRule.objects.filter(saved_view=instance).delete() + for rule_data in rules_data: + SavedViewFilterRule.objects.create(saved_view=instance, **rule_data) + return instance + + def create(self, validated_data): + rules_data = validated_data.pop('filter_rules') + saved_view = SavedView.objects.create(**validated_data) + for rule_data in rules_data: + SavedViewFilterRule.objects.create(saved_view=saved_view, **rule_data) + return saved_view + + class PostDocumentSerializer(serializers.Serializer): document = serializers.FileField( diff --git a/src/documents/views.py b/src/documents/views.py index b42ae1f96..36d3445c4 100755 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -38,7 +38,7 @@ from .filters import ( DocumentTypeFilterSet, LogFilterSet ) -from .models import Correspondent, Document, Log, Tag, DocumentType +from .models import Correspondent, Document, Log, Tag, DocumentType, SavedView from .parsers import get_parser_class_for_mime_type from .serialisers import ( CorrespondentSerializer, @@ -46,7 +46,8 @@ from .serialisers import ( LogSerializer, TagSerializer, DocumentTypeSerializer, - PostDocumentSerializer + PostDocumentSerializer, + SavedViewSerializer ) @@ -240,6 +241,22 @@ class LogViewSet(ReadOnlyModelViewSet): ordering_fields = ("created",) +class SavedViewViewSet(ModelViewSet): + model = SavedView + + queryset = SavedView.objects.all() + serializer_class = SavedViewSerializer + pagination_class = StandardPagination + permission_classes = (IsAuthenticated,) + + def get_queryset(self): + user = self.request.user + return SavedView.objects.filter(user=user) + + def perform_create(self, serializer): + serializer.save(user=self.request.user) + + class PostDocumentView(APIView): permission_classes = (IsAuthenticated,) diff --git a/src/paperless/urls.py b/src/paperless/urls.py index 9b390b139..079971bb3 100755 --- a/src/paperless/urls.py +++ b/src/paperless/urls.py @@ -17,7 +17,8 @@ from documents.views import ( IndexView, SearchAutoCompleteView, StatisticsView, - PostDocumentView + PostDocumentView, + SavedViewViewSet ) from paperless.views import FaviconView @@ -27,6 +28,7 @@ api_router.register(r"document_types", DocumentTypeViewSet) api_router.register(r"documents", DocumentViewSet) api_router.register(r"logs", LogViewSet) api_router.register(r"tags", TagViewSet) +api_router.register(r"saved_views", SavedViewViewSet) urlpatterns = [ From 8ce4434ba91aca0d280438ff16cd9c530ec2295b Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sat, 12 Dec 2020 09:01:48 -0800 Subject: [PATCH 0064/1708] Move date dropdown component --- src-ui/src/app/app.module.ts | 2 +- .../filter-dropdown-date.component.html | 0 .../filter-dropdown-date.component.scss | 0 .../filter-dropdown-date.component.spec.ts | 0 .../filter-dropdown-date/filter-dropdown-date.component.ts | 7 +++---- .../components/filter-editor/filter-editor.component.ts | 2 +- 6 files changed, 5 insertions(+), 6 deletions(-) rename src-ui/src/app/components/filter-editor/{filter-dropdown => }/filter-dropdown-date/filter-dropdown-date.component.html (100%) rename src-ui/src/app/components/filter-editor/{filter-dropdown => }/filter-dropdown-date/filter-dropdown-date.component.scss (100%) rename src-ui/src/app/components/filter-editor/{filter-dropdown => }/filter-dropdown-date/filter-dropdown-date.component.spec.ts (100%) rename src-ui/src/app/components/filter-editor/{filter-dropdown => }/filter-dropdown-date/filter-dropdown-date.component.ts (89%) diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index 4c24123e6..3021e417b 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -29,7 +29,7 @@ import { AppFrameComponent } from './components/app-frame/app-frame.component'; import { ToastsComponent } from './components/common/toasts/toasts.component'; import { FilterEditorComponent } from './components/filter-editor/filter-editor.component'; import { FilterDropdownComponent } from './components/filter-editor/filter-dropdown/filter-dropdown.component'; -import { FilterDropdownDateComponent } from './components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component'; +import { FilterDropdownDateComponent } from './components/filter-editor/filter-dropdown-date/filter-dropdown-date.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 { NgxFileDropModule } from 'ngx-file-drop'; diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html similarity index 100% rename from src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.html rename to src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.scss b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.scss similarity index 100% rename from src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.scss rename to src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.scss diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.spec.ts b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.spec.ts similarity index 100% rename from src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.spec.ts rename to src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.spec.ts diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts similarity index 89% rename from src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.ts rename to src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts index 2973a25eb..9acfb44f8 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-date/filter-dropdown-date.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts @@ -2,7 +2,6 @@ import { Component, EventEmitter, Input, OnInit, Output, ElementRef, ViewChild } import { FilterRule } from 'src/app/data/filter-rule'; import { FilterRuleType, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; import { ObjectWithId } from 'src/app/data/object-with-id'; -import { FilterDropdownComponent } from '../filter-dropdown.component' import { NgbDate, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; @Component({ @@ -10,7 +9,7 @@ import { NgbDate, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; templateUrl: './filter-dropdown-date.component.html', styleUrls: ['./filter-dropdown-date.component.scss'] }) -export class FilterDropdownDateComponent extends FilterDropdownComponent { +export class FilterDropdownDateComponent { @Input() filterRuleTypeIDs: number[] = [] @@ -19,13 +18,13 @@ export class FilterDropdownDateComponent extends FilterDropdownComponent { selected = new EventEmitter() filterRuleTypes: FilterRuleType[] = [] + title: string dateAfter: NgbDateStruct dateBefore: NgbDateStruct ngOnInit(): void { this.filterRuleTypes = this.filterRuleTypeIDs.map(id => FILTER_RULE_TYPES.find(rt => rt.id == id)) - this.filterRuleTypeID = this.filterRuleTypeIDs[0] - super.ngOnInit() + this.title = this.filterRuleTypes[0].displayName } setDateQuickFilter(range: any) { diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/filter-editor/filter-editor.component.ts index 05dd3a92a..3d006a76e 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.ts @@ -10,7 +10,7 @@ import { CorrespondentService } from 'src/app/services/rest/correspondent.servic import { DocumentTypeService } from 'src/app/services/rest/document-type.service'; import { TagService } from 'src/app/services/rest/tag.service'; import { FilterDropdownComponent } from './filter-dropdown/filter-dropdown.component' -import { FilterDropdownDateComponent } from './filter-dropdown/filter-dropdown-date/filter-dropdown-date.component' +import { FilterDropdownDateComponent } from './filter-dropdown-date/filter-dropdown-date.component' import { fromEvent } from 'rxjs'; import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; From e215e11417448a4294f507260d718d11bd5f9e0e Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sat, 12 Dec 2020 22:53:34 -0800 Subject: [PATCH 0065/1708] Completely refactored because programming Extracted filter editor to service Made all components actually reactive --- src-ui/src/app/app.module.ts | 2 + .../document-list.component.html | 2 +- .../document-list/document-list.component.ts | 34 ++-- .../filter-dropdown-date.component.html | 4 +- .../filter-dropdown-date.component.ts | 46 ++--- .../filter-dropdown-button.component.html | 12 ++ .../filter-dropdown-button.component.scss | 4 + .../filter-dropdown-button.component.spec.ts | 25 +++ .../filter-dropdown-button.component.ts | 32 +++ .../filter-dropdown.component.html | 17 +- .../filter-dropdown.component.scss | 5 - .../filter-dropdown.component.ts | 29 ++- .../filter-editor.component.html | 11 +- .../filter-editor/filter-editor.component.ts | 164 ++++----------- .../services/document-list-view.service.ts | 6 +- .../filter-editor-view.service.spec.ts | 16 ++ .../services/filter-editor-view.service.ts | 188 ++++++++++++++++++ 17 files changed, 395 insertions(+), 202 deletions(-) create mode 100644 src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.html create mode 100644 src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.scss create mode 100644 src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.spec.ts create mode 100644 src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.ts create mode 100644 src-ui/src/app/services/filter-editor-view.service.spec.ts create mode 100644 src-ui/src/app/services/filter-editor-view.service.ts diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index 3021e417b..4d31fff18 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -29,6 +29,7 @@ import { AppFrameComponent } from './components/app-frame/app-frame.component'; import { ToastsComponent } from './components/common/toasts/toasts.component'; import { FilterEditorComponent } from './components/filter-editor/filter-editor.component'; import { FilterDropdownComponent } from './components/filter-editor/filter-dropdown/filter-dropdown.component'; +import { FilterDropdownButtonComponent } from './components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component'; import { FilterDropdownDateComponent } from './components/filter-editor/filter-dropdown-date/filter-dropdown-date.component'; import { 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'; @@ -77,6 +78,7 @@ import { FilterPipe } from './pipes/filter.pipe'; ToastsComponent, FilterEditorComponent, FilterDropdownComponent, + FilterDropdownButtonComponent, FilterDropdownDateComponent, DocumentCardLargeComponent, DocumentCardSmallComponent, diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index a6ec1b741..0fd139b89 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -64,7 +64,7 @@ <div class="card w-100 mb-3"> <div class="card-body"> - <app-filter-editor [(filterRules)]="filterRules" (apply)="applyFilterRules()" (clear)="clearFilterRules()" #filterEditor></app-filter-editor> + <app-filter-editor [(filterEditorService)]="filterEditorService" (apply)="applyFilterRules()" (clear)="clearFilterRules()" #filterEditor></app-filter-editor> </div> </div> diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index 271f6f7e5..fe03cae80 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -6,6 +6,7 @@ import { cloneFilterRules, FilterRule } from 'src/app/data/filter-rule'; import { FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; import { SavedViewConfig } from 'src/app/data/saved-view-config'; import { DocumentListViewService } from 'src/app/services/document-list-view.service'; +import { FilterEditorViewService } from 'src/app/services/filter-editor-view.service'; import { DOCUMENT_SORT_FIELDS } from 'src/app/services/rest/document.service'; import { SavedViewConfigService } from 'src/app/services/saved-view-config.service'; import { Toast, ToastService } from 'src/app/services/toast.service'; @@ -26,6 +27,7 @@ export class DocumentListComponent implements OnInit { constructor( public list: DocumentListViewService, public savedViewConfigService: SavedViewConfigService, + public filterEditorService: FilterEditorViewService, public route: ActivatedRoute, private toastService: ToastService, public modalService: NgbModal, @@ -33,14 +35,18 @@ export class DocumentListComponent implements OnInit { displayMode = 'smallCards' // largeCards, smallCards, details - filterRules: FilterRule[] = [] - - @ViewChild('filterEditor') filterEditor: FilterEditorComponent - get isFiltered() { return this.list.filterRules?.length > 0 } + set filterRules(filterRules: FilterRule[]) { + this.filterEditorService.filterRules = filterRules + } + + get filterRules(): FilterRule[] { + return this.filterEditorService.filterRules + } + getTitle() { return this.list.savedViewTitle || "Documents" } @@ -60,28 +66,29 @@ export class DocumentListComponent implements OnInit { this.route.paramMap.subscribe(params => { if (params.has('id')) { this.list.savedView = this.savedViewConfigService.getConfig(params.get('id')) - this.filterRules = this.list.filterRules + this.filterEditorService.filterRules = this.list.filterRules this.titleService.setTitle(`${this.list.savedView.title} - ${environment.appTitle}`) } else { this.list.savedView = null - this.filterRules = this.list.filterRules + this.filterEditorService.filterRules = this.list.filterRules this.titleService.setTitle(`Documents - ${environment.appTitle}`) } this.list.clear() this.list.reload() }) + this.filterEditorService.filterRules = this.list.filterRules } applyFilterRules() { - this.list.filterRules = this.filterRules + this.list.filterRules = this.filterEditorService.filterRules } clearFilterRules() { - this.list.filterRules = this.filterRules + this.list.filterRules = this.filterEditorService.filterRules } loadViewConfig(config: SavedViewConfig) { - this.filterRules = cloneFilterRules(config.filterRules) + this.filterEditorService.filterRules = cloneFilterRules(config.filterRules) this.list.load(config) } @@ -106,15 +113,18 @@ export class DocumentListComponent implements OnInit { } clickTag(tagID: number) { - this.filterEditor.toggleFilterByItem(tagID, FILTER_HAS_TAG) + this.filterEditorService.toggleFitlerByTagID(tagID) + this.applyFilterRules() } clickCorrespondent(correspondentID: number) { - this.filterEditor.toggleFilterByItem(correspondentID, FILTER_CORRESPONDENT) + this.filterEditorService.toggleFitlerByCorrespondentID(correspondentID) + this.applyFilterRules() } clickDocumentType(documentTypeID: number) { - this.filterEditor.toggleFilterByItem(documentTypeID, FILTER_DOCUMENT_TYPE) + this.filterEditorService.toggleFitlerByDocumentTypeID(documentTypeID) + this.applyFilterRules() } } diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html index fb514d7df..a2b395c09 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html @@ -11,7 +11,7 @@ <div class="list-group-item d-flex flex-column align-items-start" role="menuitem"> <div class="mb-1"><small>Before</small></div> <div class="input-group input-group-sm"> - <input class="form-control" type="text" placeholder="yyyy-mm-dd" name="before" [(ngModel)]="dateBefore" ngbDatepicker (dateSelect)="dateSelected($event)" #dpBefore="ngbDatepicker"> + <input class="form-control" type="text" placeholder="yyyy-mm-dd" name="before" [(ngModel)]="_dateBefore" ngbDatepicker (dateSelect)="onDateSelected($event)" #dpBefore="ngbDatepicker"> <div class="input-group-append"> <button class="btn btn-outline-secondary btn-sm" (click)="dpBefore.toggle()" type="button"> <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-calendar-date" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> @@ -25,7 +25,7 @@ <div class="list-group-item d-flex flex-column align-items-start" role="menuitem"> <div class="mb-1"><small>After</small></div> <div class="input-group"> - <input class="form-control form-control-sm" type="text" placeholder="yyyy-mm-dd" name="after" [(ngModel)]="dateAfter" ngbDatepicker (dateSelect)="dateSelected($event)" #dpAfter="ngbDatepicker"> + <input class="form-control form-control-sm" type="text" placeholder="yyyy-mm-dd" name="after" [(ngModel)]="_dateAfter" ngbDatepicker (dateSelect)="onDateSelected($event)" #dpAfter="ngbDatepicker"> <div class="input-group-append"> <button class="btn btn-outline-secondary btn-sm" (click)="dpAfter.toggle()" type="button"> <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-calendar-date" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts index 9acfb44f8..0d38f5541 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts @@ -1,6 +1,5 @@ -import { Component, EventEmitter, Input, OnInit, Output, ElementRef, ViewChild } from '@angular/core'; +import { Component, EventEmitter, Input, Output, ElementRef, ViewChild } from '@angular/core'; import { FilterRule } from 'src/app/data/filter-rule'; -import { FilterRuleType, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; import { ObjectWithId } from 'src/app/data/object-with-id'; import { NgbDate, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; @@ -12,23 +11,25 @@ import { NgbDate, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; export class FilterDropdownDateComponent { @Input() - filterRuleTypeIDs: number[] = [] - - @Output() - selected = new EventEmitter() - - filterRuleTypes: FilterRuleType[] = [] - title: string - dateAfter: NgbDateStruct dateBefore: NgbDateStruct - ngOnInit(): void { - this.filterRuleTypes = this.filterRuleTypeIDs.map(id => FILTER_RULE_TYPES.find(rt => rt.id == id)) - this.title = this.filterRuleTypes[0].displayName - } + @Input() + dateAfter: NgbDateStruct + + @Input() + title: string + + @Output() + dateBeforeSet = new EventEmitter() + + @Output() + dateAfterSet = new EventEmitter() + + _dateBefore: NgbDateStruct + _dateAfter: NgbDateStruct setDateQuickFilter(range: any) { - this.dateAfter = this.dateBefore = undefined + this._dateAfter = this._dateBefore = undefined let date = new Date() let newDate: NgbDateStruct = { year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate() } switch (typeof range) { @@ -47,17 +48,12 @@ export class FilterDropdownDateComponent { default: break } - this.dateAfter = newDate - this.dateSelected(this.dateAfter) + this._dateAfter = newDate + this.onDateSelected(this._dateAfter) } - dateSelected(date:NgbDateStruct) { - let isAfter = NgbDate.from(this.dateAfter).equals(date) - - let filterRuleType = this.filterRuleTypes.find(rt => rt.filtervar.indexOf(isAfter ? 'gt' : 'lt') > -1) - if (filterRuleType) { - let dateFilterRule:FilterRule = {value: `${date.year}-${date.month.toString().padStart(2,'0')}-${date.day.toString().padStart(2,'0')}`, type: filterRuleType} - this.selected.emit(dateFilterRule) - } + onDateSelected(date:NgbDateStruct) { + let emitter = this._dateAfter && NgbDate.from(this._dateAfter).equals(date) ? this.dateAfterSet : this.dateBeforeSet + emitter.emit(date) } } diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.html new file mode 100644 index 000000000..5f12a5a17 --- /dev/null +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.html @@ -0,0 +1,12 @@ +<button class="list-group-item list-group-item-action d-flex align-items-center" 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="display == 'tag'; 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 bg-primary text-light rounded-pill ml-auto">{{item.document_count}}</div> +</button> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.scss b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.scss new file mode 100644 index 000000000..41fc6acc4 --- /dev/null +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.scss @@ -0,0 +1,4 @@ +.selected-icon { + min-width: 1em; + min-height: 1em; +} diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.spec.ts b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.spec.ts new file mode 100644 index 000000000..5cf1fefa2 --- /dev/null +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.spec.ts @@ -0,0 +1,25 @@ +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(); + }); +}); diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.ts new file mode 100644 index 000000000..847c3f12b --- /dev/null +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.ts @@ -0,0 +1,32 @@ +import { Component, EventEmitter, Input, Output } 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 { + + constructor() { } + + @Input() + item: PaperlessTag | PaperlessDocumentType | PaperlessCorrespondent + + @Input() + display: string + + @Input() + selected: boolean + + @Output() + toggle = new EventEmitter() + + + toggleItem(): void { + this.selected = !this.selected + this.toggle.emit(this.item) + } +} diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html index b43826fb2..3d47d23b7 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html @@ -3,19 +3,10 @@ <div class="dropdown-menu quick-filter shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> <div class="list-group list-group-flush"> <input class="list-group-item form-control form-control-sm" type="text" [(ngModel)]="filterText" placeholder="Filter {{title}}" (keyup.enter)="listFilterEnter()" #listFilterTextInput> - <ng-container *ngIf="(items | filter: filterText).length > 0"> - <button class="list-group-item list-group-item-action d-flex align-items-center" role="menuitem" *ngFor="let item of items | filter: filterText; let i = index" (click)="toggleItem(item)"> - <div class="selected-icon mr-1"> - <svg *ngIf="itemsActive.includes(item)" 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="display == 'tag'; 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 bg-primary text-light rounded-pill ml-auto">{{item.document_count}}</div> - </button> + <ng-container *ngIf="(items$ | async)?.results as items"> + <ng-container *ngFor="let item of items | filter: filterText; let i = index"> + <app-filter-dropdown-button [item]="item" [display]="display" [selected]="isItemSelected(item)" (toggle)="toggleItem($event)"></app-filter-dropdown-button> + </ng-container> </ng-container> </div> </div> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.scss b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.scss index 05df7b213..5551b0329 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.scss +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.scss @@ -2,9 +2,4 @@ min-width: 250px; max-height: 400px; overflow-y: scroll; - - .selected-icon { - min-width: 1em; - min-height: 1em; - } } diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts index 6f346d4b3..4c80bfb66 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts @@ -1,5 +1,6 @@ import { Component, EventEmitter, Input, OnInit, Output, ElementRef, ViewChild } from '@angular/core'; -import { FilterRuleType, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; +import { Observable } from 'rxjs'; +import { Results } from 'src/app/data/results'; import { ObjectWithId } from 'src/app/data/object-with-id'; import { FilterPipe } from 'src/app/pipes/filter.pipe'; @@ -13,29 +14,37 @@ export class FilterDropdownComponent implements OnInit { constructor(private filterPipe: FilterPipe) { } @Input() - filterRuleTypeID: number + items$: Observable<Results<ObjectWithId>> + + @Input() + itemsSelected: ObjectWithId[] + + @Input() + title: string + + @Input() + display: string @Output() toggle = new EventEmitter() @ViewChild('listFilterTextInput') listFilterTextInput: ElementRef - items: ObjectWithId[] = [] - itemsActive: ObjectWithId[] = [] - title: string filterText: string - display: string + items: ObjectWithId[] - ngOnInit(): void { - let filterRuleType: FilterRuleType = FILTER_RULE_TYPES.find(t => t.id == this.filterRuleTypeID) - this.title = filterRuleType.displayName - this.display = filterRuleType.datatype + ngOnInit() { + this.items$.subscribe(result => this.items = result.results) } 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(() => { diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.html b/src-ui/src/app/components/filter-editor/filter-editor.component.html index 368eed564..4c2e61d46 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.html +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.html @@ -3,14 +3,17 @@ <div class="text-muted mt-1">Filter by:</div> </div> <div class="col"> - <input class="form-control form-control-sm" type="text" [(ngModel)]="filterText" placeholder="Title" #filterTextInput> + <input class="form-control form-control-sm" type="text" [(ngModel)]="filterEditorService.filterText" placeholder="Title" #filterTextInput> </div> - <app-filter-dropdown class="col-auto" *ngFor="let quickFilterRuleTypeID of quickFilterRuleTypeIDs" [filterRuleTypeID]="quickFilterRuleTypeID" (toggle)="toggleFilterByItem($event, quickFilterRuleTypeID)"></app-filter-dropdown> + <app-filter-dropdown class="col-auto" [(items$)]="filterEditorService.tags$" [itemsSelected]="filterEditorService.selectedTags" [title]="'Tags'" [display]="'tag'" (toggle)="onToggleTag($event)"></app-filter-dropdown> + <app-filter-dropdown class="col-auto" [(items$)]="filterEditorService.correspondents$" [itemsSelected]="filterEditorService.selectedCorrespondents" [title]="'Correspondents'" (toggle)="onToggleCorrespondent($event)"></app-filter-dropdown> + <app-filter-dropdown class="col-auto" [(items$)]="filterEditorService.documentTypes$" [itemsSelected]="filterEditorService.selectedDocumentTypes" [title]="'Document Types'" (toggle)="onToggleDocumentType($event)"></app-filter-dropdown> - <app-filter-dropdown-date class="col-auto" *ngFor="let dateAddedFilterRuleTypeID of dateAddedFilterRuleTypeIDs" [filterRuleTypeIDs]="dateAddedFilterRuleTypeID" (selected)="setDateFilter($event)"></app-filter-dropdown-date> + <app-filter-dropdown-date class="col-auto" [dateBefore]="filterEditorService.dateCreatedBefore" [dateAfter]="filterEditorService.dateCreatedAfter" [title]="'Created'" (dateBeforeSet)="onDateCreatedBeforeSet($event)" (dateAfterSet)="onDateCreatedAfterSet($event)"></app-filter-dropdown-date> + <app-filter-dropdown-date class="col-auto" [dateBefore]="filterEditorService.dateAddedBefore" [dateAfter]="filterEditorService.dateAddedAfter" [title]="'Added'" (dateBeforeSet)="onDateAddedBeforeSet($event)" (dateAfterSet)="onDateAddedAfterSet($event)"></app-filter-dropdown-date> - <button class="btn btn-link btn-sm" [disabled]="!hasFilters()" (click)="clearSelected()"> + <button class="btn btn-link btn-sm" [disabled]="!filterEditorService.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> diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/filter-editor/filter-editor.component.ts index 3d006a76e..bb838f0be 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.ts @@ -1,14 +1,10 @@ -import { Component, EventEmitter, Input, OnInit, Output, ElementRef, AfterViewInit, QueryList, ViewChild, ViewChildren } from '@angular/core'; -import { FilterRule } from 'src/app/data/filter-rule'; -import { FilterRuleType, FILTER_RULE_TYPES, FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_TITLE, FILTER_ADDED_BEFORE, FILTER_ADDED_AFTER, FILTER_CREATED_BEFORE, FILTER_CREATED_AFTER, FILTER_CREATED_YEAR, FILTER_CREATED_MONTH, FILTER_CREATED_DAY } from 'src/app/data/filter-rule-type'; +import { Component, EventEmitter, Input, Output, ElementRef, AfterViewInit, QueryList, ViewChild, ViewChildren } from '@angular/core'; +import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'; +import { ObjectWithId } from 'src/app/data/object-with-id'; +import { FilterEditorViewService } from 'src/app/services/filter-editor-view.service' 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 { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'; -import { ObjectWithId } from 'src/app/data/object-with-id'; -import { CorrespondentService } from 'src/app/services/rest/correspondent.service'; -import { DocumentTypeService } from 'src/app/services/rest/document-type.service'; -import { TagService } from 'src/app/services/rest/tag.service'; import { FilterDropdownComponent } from './filter-dropdown/filter-dropdown.component' import { FilterDropdownDateComponent } from './filter-dropdown-date/filter-dropdown-date.component' import { fromEvent } from 'rxjs'; @@ -20,38 +16,20 @@ import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; templateUrl: './filter-editor.component.html', styleUrls: ['./filter-editor.component.scss'] }) -export class FilterEditorComponent implements OnInit, AfterViewInit { +export class FilterEditorComponent implements AfterViewInit { - constructor(private documentTypeService: DocumentTypeService, private tagService: TagService, private correspondentService: CorrespondentService) { } + constructor() { } + + @Input() + filterEditorService: FilterEditorViewService @Output() clear = new EventEmitter() - @Input() - filterRules: FilterRule[] = [] - @Output() apply = new EventEmitter() @ViewChild('filterTextInput') filterTextInput: ElementRef; - @ViewChildren(FilterDropdownComponent) quickFilterDropdowns!: QueryList<FilterDropdownComponent>; - @ViewChildren(FilterDropdownDateComponent) quickDateFilterDropdowns!: QueryList<FilterDropdownDateComponent>; - - quickFilterRuleTypeIDs: number[] = [FILTER_HAS_TAG, FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE] - dateAddedFilterRuleTypeIDs: any[] = [[FILTER_ADDED_BEFORE, FILTER_ADDED_AFTER], [FILTER_CREATED_BEFORE, FILTER_CREATED_AFTER]] - - correspondents: PaperlessCorrespondent[] = [] - tags: PaperlessTag[] = [] - documentTypes: PaperlessDocumentType[] = [] - - filterText: string - - ngOnInit(): void { - this.updateTextFilterInput() - this.tagService.listAll().subscribe(result => this.setDropdownItems(result.results, FILTER_HAS_TAG)) - this.correspondentService.listAll().subscribe(result => this.setDropdownItems(result.results, FILTER_CORRESPONDENT)) - this.documentTypeService.listAll().subscribe(result => this.setDropdownItems(result.results, FILTER_DOCUMENT_TYPE)) - } ngAfterViewInit() { fromEvent(this.filterTextInput.nativeElement,'keyup').pipe( @@ -59,120 +37,52 @@ export class FilterEditorComponent implements OnInit, AfterViewInit { distinctUntilChanged(), tap() ).subscribe((event: Event) => { - this.filterText = (event.target as HTMLInputElement).value - this.onTextFilterInput() + this.filterEditorService.filterText = (event.target as HTMLInputElement).value + this.applyFilters() }) - this.quickDateFilterDropdowns.forEach(d => this.updateDateDropdown(d)) } - setDropdownItems(items: ObjectWithId[], filterRuleTypeID: number): void { - let dropdown: FilterDropdownComponent = this.getDropdownByFilterRuleTypeID(filterRuleTypeID) - if (dropdown) { - dropdown.items = items - } - } - - updateDropdownActiveItems(dropdown: FilterDropdownComponent): void { - let activeRulesValues = this.filterRules.filter(r => r.type.id == dropdown.filterRuleTypeID).map(r => r.value) - let activeItems = [] - if (activeRulesValues.length > 0) { - activeItems = dropdown.items.filter(i => activeRulesValues.includes(i.id)) - } - dropdown.itemsActive = activeItems - } - - updateDateDropdown(dateDropdown: FilterDropdownDateComponent) { - let activeRules = this.filterRules.filter(r => dateDropdown.filterRuleTypeIDs.includes(r.type.id)) - if (activeRules.length > 0) { - activeRules.forEach(rule => { - let date = { year: rule.value.substring(0,4), month: rule.value.substring(5,7), day: rule.value.substring(8,10) } - rule.type.filtervar.indexOf('gt') > -1 ? dateDropdown.dateAfter = date : dateDropdown.dateBefore = date - }) - } else { - dateDropdown.dateAfter = dateDropdown.dateBefore = undefined - } - } - - getDropdownByFilterRuleTypeID(filterRuleTypeID: number): FilterDropdownComponent { - return this.quickFilterDropdowns.find(d => d.filterRuleTypeID == filterRuleTypeID) - } - - applySelected() { + applyFilters() { this.apply.next() } clearSelected() { - this.filterRules.splice(0,this.filterRules.length) - this.updateTextFilterInput() - this.quickFilterDropdowns.forEach(d => this.updateDropdownActiveItems(d)) - this.quickDateFilterDropdowns.forEach(d => this.updateDateDropdown(d)) + this.filterEditorService.clear() this.clear.next() } - hasFilters() { - return this.filterRules.length > 0 + onToggleTag(tag: PaperlessTag) { + this.filterEditorService.toggleFitlerByTag(tag) + this.applyFilters() } - updateTextFilterInput() { - let existingTextRule = this.filterRules.find(rule => rule.type.id == FILTER_TITLE) - if (existingTextRule) this.filterText = existingTextRule.value - else this.filterText = '' + onToggleCorrespondent(correspondent: PaperlessCorrespondent) { + this.filterEditorService.toggleFitlerByCorrespondent(correspondent) + this.applyFilters() } - onTextFilterInput() { - let text = this.filterText - let filterRules = this.filterRules - let existingRule = filterRules.find(rule => rule.type.id == FILTER_TITLE) - if (existingRule && existingRule.value == text) { - return - } else if (existingRule) { - existingRule.value = text - } else { - filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_TITLE), value: text}) - } - this.filterRules = filterRules - this.applySelected() + onToggleDocumentType(documentType: PaperlessDocumentType) { + this.filterEditorService.toggleFitlerByDocumentType(documentType) + this.applyFilters() } - toggleFilterByItem(item: any, filterRuleTypeID: number) { - let dropdown = this.getDropdownByFilterRuleTypeID(filterRuleTypeID) - if (typeof item == 'number') { - item = dropdown.items.find(i => i.id == item) - } - let filterRules = this.filterRules - let filterRuleType: FilterRuleType = FILTER_RULE_TYPES.find(t => t.id == filterRuleTypeID) - let existingRule = filterRules.find(rule => rule.type.id == filterRuleType.id) - - if (existingRule && existingRule.value == item.id) { - filterRules.splice(filterRules.indexOf(existingRule), 1) - } else if (existingRule && filterRuleType.id == FILTER_HAS_TAG) { - filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == filterRuleType.id), value: item.id}) - } else if (existingRule && existingRule.value == item.id) { - return - } else if (existingRule) { - existingRule.value = item.id - } else { - filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == filterRuleType.id), value: item.id}) - } - - this.updateDropdownActiveItems(dropdown) - - this.filterRules = filterRules - this.applySelected() + onDateCreatedBeforeSet(date: NgbDateStruct) { + this.filterEditorService.setDateCreatedBefore(date) + this.applyFilters() } - setDateFilter(newFilterRule: FilterRule) { - let filterRules = this.filterRules - let existingRule = filterRules.find(rule => rule.type.id == newFilterRule.type.id) - - if (existingRule) { - existingRule.value = newFilterRule.value - } else { - filterRules.push(newFilterRule) - } - - this.filterRules = filterRules - this.applySelected() + onDateCreatedAfterSet(date: NgbDateStruct) { + this.filterEditorService.setDateCreatedAfter(date) + this.applyFilters() } + onDateAddedBeforeSet(date: NgbDateStruct) { + this.filterEditorService.setDateAddedBefore(date) + this.applyFilters() + } + + onDateAddedAfterSet(date: NgbDateStruct) { + this.filterEditorService.setDateAddedAfter(date) + this.applyFilters() + } } diff --git a/src-ui/src/app/services/document-list-view.service.ts b/src-ui/src/app/services/document-list-view.service.ts index 811ac3c4b..8692ed1c0 100644 --- a/src-ui/src/app/services/document-list-view.service.ts +++ b/src-ui/src/app/services/document-list-view.service.ts @@ -9,7 +9,7 @@ import { DocumentService } from './rest/document.service'; /** * This service manages the document list which is displayed using the document list view. - * + * * This service also serves saved views by transparently switching between the document list * and saved views on request. See below. */ @@ -25,7 +25,7 @@ export class DocumentListViewService { currentPage = 1 currentPageSize: number = +localStorage.getItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE) || GENERAL_SETTINGS.DOCUMENT_LIST_SIZE_DEFAULT collectionSize: number - + /** * This is the current config for the document list. The service will always remember the last settings used for the document list. */ @@ -192,7 +192,7 @@ export class DocumentListViewService { } } - constructor(private documentService: DocumentService) { + constructor(private documentService: DocumentService) { let documentListViewConfigJson = sessionStorage.getItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG) if (documentListViewConfigJson) { try { diff --git a/src-ui/src/app/services/filter-editor-view.service.spec.ts b/src-ui/src/app/services/filter-editor-view.service.spec.ts new file mode 100644 index 000000000..8051bcf0d --- /dev/null +++ b/src-ui/src/app/services/filter-editor-view.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { FilterEditorViewService } from './filter-editor-view.service'; + +describe('FilterEditorViewService', () => { + let service: FilterEditorViewService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(FilterEditorViewService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src-ui/src/app/services/filter-editor-view.service.ts b/src-ui/src/app/services/filter-editor-view.service.ts new file mode 100644 index 000000000..ba7b6dd24 --- /dev/null +++ b/src-ui/src/app/services/filter-editor-view.service.ts @@ -0,0 +1,188 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +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 { ObjectWithId } from 'src/app/data/object-with-id'; +import { FilterRule } from 'src/app/data/filter-rule'; +import { FilterRuleType, FILTER_RULE_TYPES, FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_TITLE, FILTER_ADDED_BEFORE, FILTER_ADDED_AFTER, FILTER_CREATED_BEFORE, FILTER_CREATED_AFTER, FILTER_CREATED_YEAR, FILTER_CREATED_MONTH, FILTER_CREATED_DAY } from 'src/app/data/filter-rule-type'; +import { Results } from 'src/app/data/results' +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 { NgbDate, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; + +@Injectable({ + providedIn: 'root' +}) +export class FilterEditorViewService { + tags$: Observable<Results<PaperlessTag>> + correspondents$: Observable<Results<PaperlessCorrespondent>> + documentTypes$: Observable<Results<PaperlessDocumentType>> + + tags: PaperlessTag[] = [] + correspondents: PaperlessCorrespondent[] + documentTypes: PaperlessDocumentType[] = [] + + filterRules: FilterRule[] = [] + + constructor(private tagService: TagService, private documentTypeService: DocumentTypeService, private correspondentService: CorrespondentService) { + this.tags$ = this.tagService.listAll() + this.tags$.subscribe(result => this.tags = result.results) + this.correspondents$ = this.correspondentService.listAll() + this.correspondents$.subscribe(result => this.correspondents = result.results) + this.documentTypes$ = this.documentTypeService.listAll() + this.documentTypes$.subscribe(result => this.documentTypes = result.results) + } + + clear() { + this.filterRules = [] + } + + hasFilters() { + return this.filterRules.length > 0 + } + + set filterText(text: string) { + let filterRules = this.filterRules + let existingRule = filterRules.find(rule => rule.type.id == FILTER_TITLE) + if (existingRule && existingRule.value == text) { + return + } else if (existingRule) { + existingRule.value = text + } else { + filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_TITLE), value: text}) + } + this.filterRules = filterRules + } + + get filterText(): string { + let existingRule = this.filterRules.find(rule => rule.type.id == FILTER_TITLE) + return existingRule ? existingRule.value : '' + } + + get selectedTags(): PaperlessTag[] { + let tagRules: FilterRule[] = this.filterRules.filter(fr => fr.type.id == 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.type.id == 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.type.id == FILTER_DOCUMENT_TYPE) + return this.documentTypes?.filter(dt => documentTypeRules.find(dtr => dtr.value == dt.id)) + } + + toggleFitlerByTag(tag: PaperlessTag) { + this.toggleFilterByItem(tag, FILTER_HAS_TAG) + } + + toggleFitlerByCorrespondent(tag: PaperlessCorrespondent) { + this.toggleFilterByItem(tag, FILTER_CORRESPONDENT) + } + + toggleFitlerByDocumentType(tag: PaperlessDocumentType) { + this.toggleFilterByItem(tag, FILTER_DOCUMENT_TYPE) + } + + toggleFitlerByTagID(tagID: number) { + this.toggleFitlerByTag(this.tags?.find(t => t.id == tagID)) + } + + toggleFitlerByCorrespondentID(correspondentID: number) { + this.toggleFitlerByCorrespondent(this.correspondents?.find(t => t.id == correspondentID)) + } + + toggleFitlerByDocumentTypeID(documentTypeID: number) { + this.toggleFitlerByDocumentType(this.documentTypes?.find(t => t.id == documentTypeID)) + } + + private toggleFilterByItem(item: ObjectWithId, filterRuleTypeID: number) { + let filterRules = this.filterRules + let filterRuleType: FilterRuleType = FILTER_RULE_TYPES.find(t => t.id == filterRuleTypeID) + let existingRule = filterRules.find(rule => rule.type.id == filterRuleType.id) + + if (existingRule && existingRule.value == item.id) { + filterRules.splice(filterRules.indexOf(existingRule), 1) + } else if (existingRule && filterRuleType.id == FILTER_HAS_TAG) { + filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == filterRuleType.id), value: item.id}) + } else if (existingRule && existingRule.value == item.id) { + return + } else if (existingRule) { + existingRule.value = item.id + } else { + filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == filterRuleType.id), value: item.id}) + } + + this.filterRules = filterRules + } + + get dateCreatedBefore(): NgbDateStruct { + let createdBeforeRule: FilterRule = this.filterRules.find(fr => fr.type.id == FILTER_CREATED_BEFORE) + return createdBeforeRule ? { + year: createdBeforeRule.value.substring(0,4), + month: createdBeforeRule.value.substring(5,7), + day: createdBeforeRule.value.substring(8,10) + } : undefined + } + + get dateCreatedAfter(): NgbDateStruct { + let createdAfterRule: FilterRule = this.filterRules.find(fr => fr.type.id == FILTER_CREATED_AFTER) + return createdAfterRule ? { + year: createdAfterRule.value.substring(0,4), + month: createdAfterRule.value.substring(5,7), + day: createdAfterRule.value.substring(8,10) + } : undefined + } + + get dateAddedBefore(): NgbDateStruct { + let addedBeforeRule: FilterRule = this.filterRules.find(fr => fr.type.id == FILTER_ADDED_BEFORE) + return addedBeforeRule ? { + year: addedBeforeRule.value.substring(0,4), + month: addedBeforeRule.value.substring(5,7), + day: addedBeforeRule.value.substring(8,10) + } : undefined + } + + get dateAddedAfter(): NgbDateStruct { + let addedAfterRule: FilterRule = this.filterRules.find(fr => fr.type.id == FILTER_ADDED_AFTER) + return addedAfterRule ? { + year: addedAfterRule.value.substring(0,4), + month: addedAfterRule.value.substring(5,7), + day: addedAfterRule.value.substring(8,10) + } : undefined + } + + setDateCreatedBefore(date: NgbDateStruct) { + this.setDate(date, FILTER_CREATED_BEFORE) + } + + setDateCreatedAfter(date: NgbDateStruct) { + this.setDate(date, FILTER_CREATED_AFTER) + } + + setDateAddedBefore(date: NgbDateStruct) { + this.setDate(date, FILTER_ADDED_BEFORE) + } + + setDateAddedAfter(date: NgbDateStruct) { + this.setDate(date, FILTER_ADDED_AFTER) + } + + setDate(date: NgbDateStruct, dateRuleTypeID: number) { + let filterRules = this.filterRules + let existingRule = filterRules.find(rule => rule.type.id == dateRuleTypeID) + let newValue = `${date.year}-${date.month.toString().padStart(2,'0')}-${date.day.toString().padStart(2,'0')}` // YYYY-MM-DD + + if (existingRule) { + existingRule.value = newValue + } else { + filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == dateRuleTypeID), value: newValue}) + } + + this.filterRules = filterRules + } +} From 37c21e518d9d8f2728835c3406aec66fb2f618d2 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sun, 13 Dec 2020 01:27:11 -0800 Subject: [PATCH 0066/1708] set max date for date pickers --- .../filter-dropdown-date/filter-dropdown-date.component.html | 4 ++-- .../filter-dropdown-date/filter-dropdown-date.component.ts | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html index a2b395c09..83d8c6455 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html @@ -11,7 +11,7 @@ <div class="list-group-item d-flex flex-column align-items-start" role="menuitem"> <div class="mb-1"><small>Before</small></div> <div class="input-group input-group-sm"> - <input class="form-control" type="text" placeholder="yyyy-mm-dd" name="before" [(ngModel)]="_dateBefore" ngbDatepicker (dateSelect)="onDateSelected($event)" #dpBefore="ngbDatepicker"> + <input class="form-control" type="text" placeholder="yyyy-mm-dd" name="before" [(ngModel)]="_dateBefore" [maxDate]="this._maxDate" ngbDatepicker (dateSelect)="onDateSelected($event)" #dpBefore="ngbDatepicker"> <div class="input-group-append"> <button class="btn btn-outline-secondary btn-sm" (click)="dpBefore.toggle()" type="button"> <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-calendar-date" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> @@ -25,7 +25,7 @@ <div class="list-group-item d-flex flex-column align-items-start" role="menuitem"> <div class="mb-1"><small>After</small></div> <div class="input-group"> - <input class="form-control form-control-sm" type="text" placeholder="yyyy-mm-dd" name="after" [(ngModel)]="_dateAfter" ngbDatepicker (dateSelect)="onDateSelected($event)" #dpAfter="ngbDatepicker"> + <input class="form-control form-control-sm" type="text" placeholder="yyyy-mm-dd" name="after" [(ngModel)]="_dateAfter" [maxDate]="this._maxDate" ngbDatepicker (dateSelect)="onDateSelected($event)" #dpAfter="ngbDatepicker"> <div class="input-group-append"> <button class="btn btn-outline-secondary btn-sm" (click)="dpAfter.toggle()" type="button"> <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-calendar-date" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts index 0d38f5541..abe15072e 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts @@ -28,6 +28,11 @@ export class FilterDropdownDateComponent { _dateBefore: NgbDateStruct _dateAfter: NgbDateStruct + get _maxDate(): NgbDate { + let date = new Date() + return NgbDate.from({year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate()}) + } + setDateQuickFilter(range: any) { this._dateAfter = this._dateBefore = undefined let date = new Date() From 1379c039b889dbd151ad9abb20b46d7339188716 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sun, 13 Dec 2020 02:03:59 -0800 Subject: [PATCH 0067/1708] Workaround for infinte loop breaks two way binding for date picker initialization --- .../filter-dropdown-date.component.ts | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts index abe15072e..206dbe2c3 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts @@ -1,7 +1,7 @@ -import { Component, EventEmitter, Input, Output, ElementRef, ViewChild } from '@angular/core'; +import { Component, EventEmitter, Input, Output, ElementRef, ViewChild, OnChanges, SimpleChange } from '@angular/core'; import { FilterRule } from 'src/app/data/filter-rule'; import { ObjectWithId } from 'src/app/data/object-with-id'; -import { NgbDate, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; +import { NgbDate, NgbDateStruct, NgbDatepicker } from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'app-filter-dropdown-date', @@ -25,6 +25,9 @@ export class FilterDropdownDateComponent { @Output() dateAfterSet = new EventEmitter() + @ViewChild('dpAfter') dpAfter: NgbDatepicker + @ViewChild('dpBefore') dpBefore: NgbDatepicker + _dateBefore: NgbDateStruct _dateAfter: NgbDateStruct @@ -33,6 +36,25 @@ export class FilterDropdownDateComponent { return NgbDate.from({year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate()}) } + ngOnChanges(changes: SimpleChange) { + // this is a hacky workaround perhaps because of https://github.com/angular/angular/issues/11097 + let dateString: string + let dateAfterChange: SimpleChange = changes['dateAfter'] + let dateBeforeChange: SimpleChange = changes['dateBefore'] + + if (dateAfterChange && dateAfterChange.currentValue && this.dpAfter) { + let dateAfterDate = dateAfterChange.currentValue as NgbDateStruct + let dpAfterElRef: ElementRef = this.dpAfter['_elRef'] + dateString = `${dateAfterDate.year}-${dateAfterDate.month.toString().padStart(2,'0')}-${dateAfterDate.day.toString().padStart(2,'0')}` + dpAfterElRef.nativeElement.value = dateString + } else if (dateBeforeChange && dateBeforeChange.currentValue && this.dpBefore) { + let dateBeforeDate = dateBeforeChange.currentValue as NgbDateStruct + let dpBeforeElRef: ElementRef = this.dpBefore['_elRef'] + dateString = `${dateBeforeChange.currentValue.year}-${dateBeforeChange.currentValue.month.toString().padStart(2,'0')}-${dateBeforeChange.currentValue.day.toString().padStart(2,'0')}` + dpBeforeElRef.nativeElement.value = dateString + } + } + setDateQuickFilter(range: any) { this._dateAfter = this._dateBefore = undefined let date = new Date() From 30853e963efd78c6bbd81e33b0b24879c0655cd1 Mon Sep 17 00:00:00 2001 From: rYR79435 <60985157+rYR79435@users.noreply.github.com> Date: Sun, 13 Dec 2020 13:30:30 +0100 Subject: [PATCH 0068/1708] Open GitHub and Documentation links in a new tab --- src-ui/src/app/components/app-frame/app-frame.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src-ui/src/app/components/app-frame/app-frame.component.html b/src-ui/src/app/components/app-frame/app-frame.component.html index 3f326afdd..1cedeefde 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.html +++ b/src-ui/src/app/components/app-frame/app-frame.component.html @@ -132,7 +132,7 @@ </h6> <ul class="nav flex-column mb-2"> <li class="nav-item"> - <a class="nav-link" 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"> <use xlink:href="assets/bootstrap-icons.svg#question-circle"/> </svg> @@ -140,7 +140,7 @@ </a> </li> <li class="nav-item"> - <a class="nav-link" 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"> <use xlink:href="assets/bootstrap-icons.svg#link"/> </svg> From 7906d8fef15ec985d066e5022120c55448592d36 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Sun, 13 Dec 2020 14:10:55 +0100 Subject: [PATCH 0069/1708] selection for small cards --- .../document-card-small.component.html | 10 +++++----- .../document-card-small.component.scss | 11 +++++++++++ .../document-card-small.component.ts | 15 ++++++++++++++- .../document-list/document-list.component.html | 2 +- src-ui/src/theme.scss | 1 + 5 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html index 6909a24fb..b78fedfe3 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html @@ -1,12 +1,12 @@ <div class="col p-2 h-100 document-card" style="width: 16rem;"> - <div class="card h-100 shadow-sm"> - <div class="border-bottom"> - <img class="card-img doc-img" [src]="getThumbUrl()"> + <div class="card h-100 shadow-sm" [class.card-selected]="selected"> + <div class="border-bottom" [class.doc-img-background-selected]="selected"> + <img class="card-img doc-img" [src]="getThumbUrl()" (click)="selected = !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"> - <input type="checkbox" class="custom-control-input" id="smallCardCheck{{document.id}}" [(ngModel)]="selected"> - <label class="custom-control-label" for="smallCardCheck{{document.id}}">L</label> + <input type="checkbox" class="custom-control-input" id="smallCardCheck{{document.id}}" [checked]="selected" (change)="selected = $event.target.checked"> + <label class="custom-control-label" for="smallCardCheck{{document.id}}"></label> </div> </div> diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.scss b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.scss index ba7190615..36db2203c 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.scss +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.scss @@ -1,7 +1,10 @@ +@import "/src/theme"; + .doc-img { object-fit: cover; object-position: top; height: 200px; + mix-blend-mode: multiply; } .document-card-check { @@ -10,4 +13,12 @@ .document-card:hover .document-card-check { display: block; +} + +.card-selected { + border-color: $primary; +} + +.doc-img-background-selected { + background-color: $primaryFaded; } \ No newline at end of file diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts index 037c02cf0..5d664697b 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts @@ -13,7 +13,20 @@ export class DocumentCardSmallComponent implements OnInit { constructor(private documentService: DocumentService) { } - selected = false + _selected = false + + get selected() { + return this._selected + } + + @Input() + set selected(value: boolean) { + this._selected = value + this.selectedChange.emit(value) + } + + @Output() + selectedChange = new EventEmitter<boolean>() @Input() document: PaperlessDocument diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index a87a89bbf..0c3674421 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -155,5 +155,5 @@ <div class=" m-n2 row" *ngIf="displayMode == 'smallCards'"> - <app-document-card-small [document]="d" *ngFor="let d of list.documents" (clickTag)="filterByTag($event)" (clickCorrespondent)="filterByCorrespondent($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" (clickTag)="filterByTag($event)" (clickCorrespondent)="filterByCorrespondent($event)"></app-document-card-small> </div> diff --git a/src-ui/src/theme.scss b/src-ui/src/theme.scss index 88f3ae30f..df2aea003 100644 --- a/src-ui/src/theme.scss +++ b/src-ui/src/theme.scss @@ -1,5 +1,6 @@ $paperless-green: #17541f; $primary: #17541f; +$primaryFaded: #d1ddd2; $theme-colors: ( "primary": $primary From 5bea5e75c0457ad957d6816530feabcb0bc7dad5 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Sun, 13 Dec 2020 14:28:37 +0100 Subject: [PATCH 0070/1708] Refactored delete dialog into a more generic confirm dialog --- src-ui/src/app/app.module.ts | 4 +- .../confirm-dialog.component.html} | 6 +-- .../confirm-dialog.component.scss} | 0 .../confirm-dialog.component.spec.ts} | 12 +++--- .../confirm-dialog.component.ts | 37 +++++++++++++++++++ .../delete-dialog/delete-dialog.component.ts | 31 ---------------- .../document-detail.component.ts | 13 ++++--- .../generic-list/generic-list.component.ts | 13 ++++--- 8 files changed, 64 insertions(+), 52 deletions(-) rename src-ui/src/app/components/common/{delete-dialog/delete-dialog.component.html => confirm-dialog/confirm-dialog.component.html} (67%) rename src-ui/src/app/components/common/{delete-dialog/delete-dialog.component.scss => confirm-dialog/confirm-dialog.component.scss} (100%) rename src-ui/src/app/components/common/{delete-dialog/delete-dialog.component.spec.ts => confirm-dialog/confirm-dialog.component.spec.ts} (52%) create mode 100644 src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.ts delete mode 100644 src-ui/src/app/components/common/delete-dialog/delete-dialog.component.ts diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index 0ee36b478..a1ae10d14 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -16,7 +16,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { DatePipe } from '@angular/common'; import { NotFoundComponent } from './components/not-found/not-found.component'; import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'; -import { DeleteDialogComponent } from './components/common/delete-dialog/delete-dialog.component'; +import { ConfirmDialogComponent } from './components/common/confirm-dialog/confirm-dialog.component'; import { CorrespondentEditDialogComponent } from './components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component'; import { TagEditDialogComponent } from './components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component'; import { DocumentTypeEditDialogComponent } from './components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component'; @@ -63,7 +63,7 @@ import { DocumentTitlePipe } from './pipes/document-title.pipe'; SettingsComponent, NotFoundComponent, CorrespondentEditDialogComponent, - DeleteDialogComponent, + ConfirmDialogComponent, TagEditDialogComponent, DocumentTypeEditDialogComponent, TagComponent, diff --git a/src-ui/src/app/components/common/delete-dialog/delete-dialog.component.html b/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.html similarity index 67% rename from src-ui/src/app/components/common/delete-dialog/delete-dialog.component.html rename to src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.html index 2de507549..53b613244 100644 --- a/src-ui/src/app/components/common/delete-dialog/delete-dialog.component.html +++ b/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.html @@ -5,10 +5,10 @@ </button> </div> <div class="modal-body"> - <p><b>{{message}}</b></p> - <p *ngIf="message2">{{message2}}</p> + <p *ngIf="messageBold"><b>{{messageBold}}</b></p> + <p *ngIf="message">{{message}}</p> </div> <div class="modal-footer"> <button type="button" class="btn btn-outline-dark" (click)="cancelClicked()">Cancel</button> - <button type="button" class="btn btn-danger" (click)="deleteClicked.emit()">Delete</button> + <button type="button" class="btn" [class]="btnClass" (click)="confirmClicked.emit()">{{btnCaption}}</button> </div> \ No newline at end of file diff --git a/src-ui/src/app/components/common/delete-dialog/delete-dialog.component.scss b/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.scss similarity index 100% rename from src-ui/src/app/components/common/delete-dialog/delete-dialog.component.scss rename to src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.scss diff --git a/src-ui/src/app/components/common/delete-dialog/delete-dialog.component.spec.ts b/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.spec.ts similarity index 52% rename from src-ui/src/app/components/common/delete-dialog/delete-dialog.component.spec.ts rename to src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.spec.ts index 33c7d6e88..fe08dc57a 100644 --- a/src-ui/src/app/components/common/delete-dialog/delete-dialog.component.spec.ts +++ b/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.spec.ts @@ -1,20 +1,20 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { DeleteDialogComponent } from './delete-dialog.component'; +import { ConfirmDialogComponent } from './confirm-dialog.component'; -describe('DeleteDialogComponent', () => { - let component: DeleteDialogComponent; - let fixture: ComponentFixture<DeleteDialogComponent>; +describe('ConfirmDialogComponent', () => { + let component: ConfirmDialogComponent; + let fixture: ComponentFixture<ConfirmDialogComponent>; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ DeleteDialogComponent ] + declarations: [ ConfirmDialogComponent ] }) .compileComponents(); }); beforeEach(() => { - fixture = TestBed.createComponent(DeleteDialogComponent); + fixture = TestBed.createComponent(ConfirmDialogComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.ts b/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.ts new file mode 100644 index 000000000..e207f4598 --- /dev/null +++ b/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.ts @@ -0,0 +1,37 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'app-confirm-dialog', + templateUrl: './confirm-dialog.component.html', + styleUrls: ['./confirm-dialog.component.scss'] +}) +export class ConfirmDialogComponent implements OnInit { + + constructor(public activeModal: NgbActiveModal) { } + + @Output() + public confirmClicked = new EventEmitter() + + @Input() + title = "Confirmation" + + @Input() + messageBold + + @Input() + message + + @Input() + btnClass = "btn-primary" + + @Input() + btnCaption = "Confirm" + + ngOnInit(): void { + } + + cancelClicked() { + this.activeModal.close() + } +} diff --git a/src-ui/src/app/components/common/delete-dialog/delete-dialog.component.ts b/src-ui/src/app/components/common/delete-dialog/delete-dialog.component.ts deleted file mode 100644 index 20114c78c..000000000 --- a/src-ui/src/app/components/common/delete-dialog/delete-dialog.component.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; - -@Component({ - selector: 'app-delete-dialog', - templateUrl: './delete-dialog.component.html', - styleUrls: ['./delete-dialog.component.scss'] -}) -export class DeleteDialogComponent implements OnInit { - - constructor(public activeModal: NgbActiveModal) { } - - @Output() - public deleteClicked = new EventEmitter() - - @Input() - title = "Delete confirmation" - - @Input() - message = "Do you really want to delete this?" - - @Input() - message2 - - ngOnInit(): void { - } - - cancelClicked() { - this.activeModal.close() - } -} diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index c80a8b1ce..4aac9c769 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -13,7 +13,7 @@ import { CorrespondentService } from 'src/app/services/rest/correspondent.servic import { DocumentTypeService } from 'src/app/services/rest/document-type.service'; import { DocumentService } from 'src/app/services/rest/document.service'; import { environment } from 'src/environments/environment'; -import { DeleteDialogComponent } from '../common/delete-dialog/delete-dialog.component'; +import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'; import { CorrespondentEditDialogComponent } from '../manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component'; import { DocumentTypeEditDialogComponent } from '../manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component'; @@ -155,10 +155,13 @@ export class DocumentDetailComponent implements OnInit { } delete() { - let modal = this.modalService.open(DeleteDialogComponent, {backdrop: 'static'}) - modal.componentInstance.message = `Do you really want to delete document '${this.document.title}'?` - modal.componentInstance.message2 = `The files for this document will be deleted permanently. This operation cannot be undone.` - modal.componentInstance.deleteClicked.subscribe(() => { + let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'}) + modal.componentInstance.title = "Confirm delete" + modal.componentInstance.messageBold = `Do you really want to delete document '${this.document.title}'?` + modal.componentInstance.message = `The files for this document will be deleted permanently. This operation cannot be undone.` + modal.componentInstance.btnClass = "btn-danger" + modal.componentInstance.btnCaption = "Delete document" + modal.componentInstance.confirmClicked.subscribe(() => { this.documentsService.delete(this.document).subscribe(() => { modal.close() this.close() diff --git a/src-ui/src/app/components/manage/generic-list/generic-list.component.ts b/src-ui/src/app/components/manage/generic-list/generic-list.component.ts index d5477d010..59a5f09ed 100644 --- a/src-ui/src/app/components/manage/generic-list/generic-list.component.ts +++ b/src-ui/src/app/components/manage/generic-list/generic-list.component.ts @@ -4,7 +4,7 @@ import { MatchingModel, MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/mat import { ObjectWithId } from 'src/app/data/object-with-id'; import { SortableDirective, SortEvent } from 'src/app/directives/sortable.directive'; import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'; -import { DeleteDialogComponent } from '../../common/delete-dialog/delete-dialog.component'; +import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'; @Directive() export abstract class GenericListComponent<T extends ObjectWithId> implements OnInit { @@ -88,10 +88,13 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On } openDeleteDialog(object: T) { - var activeModal = this.modalService.open(DeleteDialogComponent, {backdrop: 'static'}) - activeModal.componentInstance.message = `Do you really want to delete ${this.getObjectName(object)}?` - activeModal.componentInstance.message2 = "Associated documents will not be deleted." - activeModal.componentInstance.deleteClicked.subscribe(() => { + var activeModal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'}) + activeModal.componentInstance.title = "Confirm delete" + activeModal.componentInstance.messageBold = `Do you really want to delete ${this.getObjectName(object)}?` + activeModal.componentInstance.message = "Associated documents will not be deleted." + activeModal.componentInstance.btnClass = "btn-danger" + activeModal.componentInstance.btnCaption = "Delete" + activeModal.componentInstance.confirmPressed.subscribe(() => { this.service.delete(object).subscribe(_ => { activeModal.close() this.reloadData() From 3089b049cfaf8bbe1628671531ef185001db54e7 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Sun, 13 Dec 2020 14:56:44 +0100 Subject: [PATCH 0071/1708] refactored metadata views --- src-ui/src/app/app.module.ts | 4 +- .../document-detail.component.html | 49 +------------------ .../metadata-collapse.component.html | 23 +++++++++ .../metadata-collapse.component.scss | 0 .../metadata-collapse.component.spec.ts | 25 ++++++++++ .../metadata-collapse.component.ts | 23 +++++++++ 6 files changed, 76 insertions(+), 48 deletions(-) create mode 100644 src-ui/src/app/components/document-detail/metadata-collapse/metadata-collapse.component.html create mode 100644 src-ui/src/app/components/document-detail/metadata-collapse/metadata-collapse.component.scss create mode 100644 src-ui/src/app/components/document-detail/metadata-collapse/metadata-collapse.component.spec.ts create mode 100644 src-ui/src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index a1ae10d14..5b92364d2 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -49,6 +49,7 @@ import { WelcomeWidgetComponent } from './components/dashboard/widgets/welcome-w import { YesNoPipe } from './pipes/yes-no.pipe'; import { FileSizePipe } from './pipes/file-size.pipe'; import { DocumentTitlePipe } from './pipes/document-title.pipe'; +import { MetadataCollapseComponent } from './components/document-detail/metadata-collapse/metadata-collapse.component'; @NgModule({ declarations: [ @@ -89,7 +90,8 @@ import { DocumentTitlePipe } from './pipes/document-title.pipe'; WelcomeWidgetComponent, YesNoPipe, FileSizePipe, - DocumentTitlePipe + DocumentTitlePipe, + MetadataCollapseComponent ], imports: [ BrowserModule, diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html index f9f6e57ef..c0114f709 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.html +++ b/src-ui/src/app/components/document-detail/document-detail.component.html @@ -110,53 +110,8 @@ </tbody> </table> - <h6 *ngIf="metadata?.original_metadata.length > 0"> - <button type="button" class="btn btn-outline-secondary btn-sm mr-2" - (click)="expandOriginalMetadata = !expandOriginalMetadata" aria-controls="collapseExample"> - <svg class="buttonicon" fill="currentColor" *ngIf="!expandOriginalMetadata"> - <use xlink:href="assets/bootstrap-icons.svg#caret-down" /> - </svg> - <svg class="buttonicon" fill="currentColor" *ngIf="expandOriginalMetadata"> - <use xlink:href="assets/bootstrap-icons.svg#caret-up" /> - </svg> - </button> - Original document metadata - </h6> - - <div #collapse="ngbCollapse" [(ngbCollapse)]="!expandOriginalMetadata"> - <table class="table table-borderless"> - <tbody> - <tr *ngFor="let m of metadata?.original_metadata"> - <td>{{m.prefix}}:{{m.key}}</td> - <td>{{m.value}}</td> - </tr> - </tbody> - </table> - </div> - - <h6 *ngIf="metadata?.has_archive_version && metadata?.archive_metadata.length > 0"> - <button type="button" class="btn btn-outline-secondary btn-sm mr-2" - (click)="expandArchivedMetadata = !expandArchivedMetadata" aria-controls="collapseExample"> - <svg class="buttonicon" fill="currentColor" *ngIf="!expandArchivedMetadata"> - <use xlink:href="assets/bootstrap-icons.svg#caret-down" /> - </svg> - <svg class="buttonicon" fill="currentColor" *ngIf="expandArchivedMetadata"> - <use xlink:href="assets/bootstrap-icons.svg#caret-up" /> - </svg> - </button> - Archived document metadata - </h6> - - <div #collapse="ngbCollapse" [(ngbCollapse)]="!expandArchivedMetadata"> - <table class="table table-borderless"> - <tbody> - <tr *ngFor="let m of metadata?.archive_metadata"> - <td>{{m.prefix}}:{{m.key}}</td> - <td>{{m.value}}</td> - </tr> - </tbody> - </table> - </div> + <app-metadata-collapse title="Original document metadata" [metadata]="metadata.original_metadata" *ngIf="metadata?.original_metadata.length > 0"></app-metadata-collapse> + <app-metadata-collapse title="Archived document metadata" [metadata]="metadata.archive_metadata" *ngIf="metadata?.archive_metadata.length > 0"></app-metadata-collapse> </ng-template> </li> diff --git a/src-ui/src/app/components/document-detail/metadata-collapse/metadata-collapse.component.html b/src-ui/src/app/components/document-detail/metadata-collapse/metadata-collapse.component.html new file mode 100644 index 000000000..e8fda1d0b --- /dev/null +++ b/src-ui/src/app/components/document-detail/metadata-collapse/metadata-collapse.component.html @@ -0,0 +1,23 @@ +<h6> + <button type="button" class="btn btn-outline-secondary btn-sm mr-2" + (click)="expand = !expand"> + <svg class="buttonicon" fill="currentColor" *ngIf="!expand"> + <use xlink:href="assets/bootstrap-icons.svg#caret-down" /> + </svg> + <svg class="buttonicon" fill="currentColor" *ngIf="expand"> + <use xlink:href="assets/bootstrap-icons.svg#caret-up" /> + </svg> + </button> + {{title}} +</h6> + +<div #collapse="ngbCollapse" [(ngbCollapse)]="!expand"> + <table class="table table-borderless"> + <tbody> + <tr *ngFor="let m of metadata"> + <td>{{m.prefix}}:{{m.key}}</td> + <td>{{m.value}}</td> + </tr> + </tbody> + </table> +</div> \ No newline at end of file diff --git a/src-ui/src/app/components/document-detail/metadata-collapse/metadata-collapse.component.scss b/src-ui/src/app/components/document-detail/metadata-collapse/metadata-collapse.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src-ui/src/app/components/document-detail/metadata-collapse/metadata-collapse.component.spec.ts b/src-ui/src/app/components/document-detail/metadata-collapse/metadata-collapse.component.spec.ts new file mode 100644 index 000000000..2bd96760b --- /dev/null +++ b/src-ui/src/app/components/document-detail/metadata-collapse/metadata-collapse.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MetadataCollapseComponent } from './metadata-collapse.component'; + +describe('MetadataCollapseComponent', () => { + let component: MetadataCollapseComponent; + let fixture: ComponentFixture<MetadataCollapseComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ MetadataCollapseComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MetadataCollapseComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src-ui/src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts b/src-ui/src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts new file mode 100644 index 000000000..160274e41 --- /dev/null +++ b/src-ui/src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts @@ -0,0 +1,23 @@ +import { Component, Input, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-metadata-collapse', + templateUrl: './metadata-collapse.component.html', + styleUrls: ['./metadata-collapse.component.scss'] +}) +export class MetadataCollapseComponent implements OnInit { + + constructor() { } + + expand = false + + @Input() + metadata + + @Input() + title = "Metadata" + + ngOnInit(): void { + } + +} From b5a85caa72422763c29dbf6baf10af8b19e0b564 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Sun, 13 Dec 2020 15:20:24 +0100 Subject: [PATCH 0072/1708] confirm dialogs for remove operations --- .../document-list/document-list.component.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index 36c70a00e..ce4ebec73 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -182,7 +182,14 @@ export class DocumentListComponent implements OnInit { } bulkRemoveCorrespondent() { - this.executeBulkOperation('set_correspondent', {"correspondent": null}).subscribe(r => {}) + 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() { @@ -202,7 +209,14 @@ export class DocumentListComponent implements OnInit { } bulkRemoveDocumentType() { - this.executeBulkOperation('set_document_type', {"document_type": null}).subscribe(r => {}) + 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() { From 2dc3019083a5ef7de57df74b2dc3cad8df49eb99 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Sun, 13 Dec 2020 15:28:20 +0100 Subject: [PATCH 0073/1708] table selection highlighting --- .../components/document-list/document-list.component.html | 2 +- .../components/document-list/document-list.component.scss | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index 0c3674421..396e7e12d 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -119,7 +119,7 @@ <th class="d-none d-xl-table-cell">Added</th> </thead> <tbody> - <tr *ngFor="let d of list.documents"> + <tr *ngFor="let d of list.documents" [ngClass]="list.isSelected(d) ? 'table-row-selected' : ''"> <td> <div class="custom-control custom-checkbox"> <input type="checkbox" class="custom-control-input" id="docCheck{{d.id}}" [checked]="list.isSelected(d)" (change)="list.setSelected(d, $event.target.checked)"> diff --git a/src-ui/src/app/components/document-list/document-list.component.scss b/src-ui/src/app/components/document-list/document-list.component.scss index e69de29bb..b9553930b 100644 --- a/src-ui/src/app/components/document-list/document-list.component.scss +++ b/src-ui/src/app/components/document-list/document-list.component.scss @@ -0,0 +1,5 @@ +@import "/src/theme"; + +.table-row-selected { + background-color: $primaryFaded; +} \ No newline at end of file From 771223030005a160d28cd93ddbdadc214136e992 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sun, 13 Dec 2020 10:18:03 -0800 Subject: [PATCH 0074/1708] remove unneeded display Input --- .../filter-dropdown-button.component.html | 2 +- .../filter-dropdown-button.component.ts | 14 +++++++------- .../filter-dropdown/filter-dropdown.component.html | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.html index 5f12a5a17..10068c675 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.html @@ -5,7 +5,7 @@ </svg> </div> <div class="mr-1"> - <app-tag *ngIf="display == 'tag'; else displayName" [tag]="item" [clickable]="true" linkTitle="Filter by tag"></app-tag> + <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 bg-primary text-light rounded-pill ml-auto">{{item.document_count}}</div> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.ts index 847c3f12b..d3ddd3cbf 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +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'; @@ -8,22 +8,22 @@ import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'; templateUrl: './filter-dropdown-button.component.html', styleUrls: ['./filter-dropdown-button.component.scss'] }) -export class FilterDropdownButtonComponent { - - constructor() { } +export class FilterDropdownButtonComponent implements OnInit { @Input() item: PaperlessTag | PaperlessDocumentType | PaperlessCorrespondent - @Input() - display: string - @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 diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html index 3d47d23b7..47a46762d 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html @@ -5,7 +5,7 @@ <input class="list-group-item form-control form-control-sm" type="text" [(ngModel)]="filterText" placeholder="Filter {{title}}" (keyup.enter)="listFilterEnter()" #listFilterTextInput> <ng-container *ngIf="(items$ | async)?.results as items"> <ng-container *ngFor="let item of items | filter: filterText; let i = index"> - <app-filter-dropdown-button [item]="item" [display]="display" [selected]="isItemSelected(item)" (toggle)="toggleItem($event)"></app-filter-dropdown-button> + <app-filter-dropdown-button [item]="item" [selected]="isItemSelected(item)" (toggle)="toggleItem($event)"></app-filter-dropdown-button> </ng-container> </ng-container> </div> From 6f684f80705e715ad36b2c0fc823695c80912865 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sun, 13 Dec 2020 10:24:20 -0800 Subject: [PATCH 0075/1708] Dropdown components now accept lists not observables --- .../filter-dropdown/filter-dropdown.component.html | 2 +- .../filter-dropdown/filter-dropdown.component.ts | 7 +------ .../components/filter-editor/filter-editor.component.html | 6 +++--- src-ui/src/app/services/filter-editor-view.service.ts | 6 +++--- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html index 47a46762d..8c48f0e2f 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html @@ -3,7 +3,7 @@ <div class="dropdown-menu quick-filter shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> <div class="list-group list-group-flush"> <input class="list-group-item form-control form-control-sm" type="text" [(ngModel)]="filterText" placeholder="Filter {{title}}" (keyup.enter)="listFilterEnter()" #listFilterTextInput> - <ng-container *ngIf="(items$ | async)?.results as items"> + <ng-container *ngIf="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> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts index 4c80bfb66..a54455153 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts @@ -14,7 +14,7 @@ export class FilterDropdownComponent implements OnInit { constructor(private filterPipe: FilterPipe) { } @Input() - items$: Observable<Results<ObjectWithId>> + items: ObjectWithId[] @Input() itemsSelected: ObjectWithId[] @@ -31,11 +31,6 @@ export class FilterDropdownComponent implements OnInit { @ViewChild('listFilterTextInput') listFilterTextInput: ElementRef filterText: string - items: ObjectWithId[] - - ngOnInit() { - this.items$.subscribe(result => this.items = result.results) - } toggleItem(item: ObjectWithId): void { this.toggle.emit(item) diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.html b/src-ui/src/app/components/filter-editor/filter-editor.component.html index 4c2e61d46..9c55c1e7e 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.html +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.html @@ -6,9 +6,9 @@ <input class="form-control form-control-sm" type="text" [(ngModel)]="filterEditorService.filterText" placeholder="Title" #filterTextInput> </div> - <app-filter-dropdown class="col-auto" [(items$)]="filterEditorService.tags$" [itemsSelected]="filterEditorService.selectedTags" [title]="'Tags'" [display]="'tag'" (toggle)="onToggleTag($event)"></app-filter-dropdown> - <app-filter-dropdown class="col-auto" [(items$)]="filterEditorService.correspondents$" [itemsSelected]="filterEditorService.selectedCorrespondents" [title]="'Correspondents'" (toggle)="onToggleCorrespondent($event)"></app-filter-dropdown> - <app-filter-dropdown class="col-auto" [(items$)]="filterEditorService.documentTypes$" [itemsSelected]="filterEditorService.selectedDocumentTypes" [title]="'Document Types'" (toggle)="onToggleDocumentType($event)"></app-filter-dropdown> + <app-filter-dropdown class="col-auto" [(items)]="filterEditorService.tags" [itemsSelected]="filterEditorService.selectedTags" [title]="'Tags'" [display]="'tag'" (toggle)="onToggleTag($event)"></app-filter-dropdown> + <app-filter-dropdown class="col-auto" [(items)]="filterEditorService.correspondents" [itemsSelected]="filterEditorService.selectedCorrespondents" [title]="'Correspondents'" (toggle)="onToggleCorrespondent($event)"></app-filter-dropdown> + <app-filter-dropdown class="col-auto" [(items)]="filterEditorService.documentTypes" [itemsSelected]="filterEditorService.selectedDocumentTypes" [title]="'Document Types'" (toggle)="onToggleDocumentType($event)"></app-filter-dropdown> <app-filter-dropdown-date class="col-auto" [dateBefore]="filterEditorService.dateCreatedBefore" [dateAfter]="filterEditorService.dateCreatedAfter" [title]="'Created'" (dateBeforeSet)="onDateCreatedBeforeSet($event)" (dateAfterSet)="onDateCreatedAfterSet($event)"></app-filter-dropdown-date> <app-filter-dropdown-date class="col-auto" [dateBefore]="filterEditorService.dateAddedBefore" [dateAfter]="filterEditorService.dateAddedAfter" [title]="'Added'" (dateBeforeSet)="onDateAddedBeforeSet($event)" (dateAfterSet)="onDateAddedAfterSet($event)"></app-filter-dropdown-date> diff --git a/src-ui/src/app/services/filter-editor-view.service.ts b/src-ui/src/app/services/filter-editor-view.service.ts index ba7b6dd24..b436ecde4 100644 --- a/src-ui/src/app/services/filter-editor-view.service.ts +++ b/src-ui/src/app/services/filter-editor-view.service.ts @@ -16,9 +16,9 @@ import { NgbDate, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; providedIn: 'root' }) export class FilterEditorViewService { - tags$: Observable<Results<PaperlessTag>> - correspondents$: Observable<Results<PaperlessCorrespondent>> - documentTypes$: Observable<Results<PaperlessDocumentType>> + private tags$: Observable<Results<PaperlessTag>> + private correspondents$: Observable<Results<PaperlessCorrespondent>> + private documentTypes$: Observable<Results<PaperlessDocumentType>> tags: PaperlessTag[] = [] correspondents: PaperlessCorrespondent[] From bb1725c7dd48f3c018a249b7d28ab25cc83a32f0 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sun, 13 Dec 2020 10:25:51 -0800 Subject: [PATCH 0076/1708] Typescript cleanup --- .../filter-dropdown/filter-dropdown.component.ts | 4 ++-- .../app/components/filter-editor/filter-editor.component.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts index a54455153..720c86a94 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, OnInit, Output, ElementRef, ViewChild } from '@angular/core'; +import { Component, EventEmitter, Input, Output, ElementRef, ViewChild } from '@angular/core'; import { Observable } from 'rxjs'; import { Results } from 'src/app/data/results'; import { ObjectWithId } from 'src/app/data/object-with-id'; @@ -9,7 +9,7 @@ import { FilterPipe } from 'src/app/pipes/filter.pipe'; templateUrl: './filter-dropdown.component.html', styleUrls: ['./filter-dropdown.component.scss'] }) -export class FilterDropdownComponent implements OnInit { +export class FilterDropdownComponent { constructor(private filterPipe: FilterPipe) { } diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.html b/src-ui/src/app/components/filter-editor/filter-editor.component.html index 9c55c1e7e..eb322414d 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.html +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.html @@ -6,7 +6,7 @@ <input class="form-control form-control-sm" type="text" [(ngModel)]="filterEditorService.filterText" placeholder="Title" #filterTextInput> </div> - <app-filter-dropdown class="col-auto" [(items)]="filterEditorService.tags" [itemsSelected]="filterEditorService.selectedTags" [title]="'Tags'" [display]="'tag'" (toggle)="onToggleTag($event)"></app-filter-dropdown> + <app-filter-dropdown class="col-auto" [(items)]="filterEditorService.tags" [itemsSelected]="filterEditorService.selectedTags" [title]="'Tags'" (toggle)="onToggleTag($event)"></app-filter-dropdown> <app-filter-dropdown class="col-auto" [(items)]="filterEditorService.correspondents" [itemsSelected]="filterEditorService.selectedCorrespondents" [title]="'Correspondents'" (toggle)="onToggleCorrespondent($event)"></app-filter-dropdown> <app-filter-dropdown class="col-auto" [(items)]="filterEditorService.documentTypes" [itemsSelected]="filterEditorService.selectedDocumentTypes" [title]="'Document Types'" (toggle)="onToggleDocumentType($event)"></app-filter-dropdown> From d6894d3c647e8ebd40d838154920b5e9abe80124 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sun, 13 Dec 2020 10:28:09 -0800 Subject: [PATCH 0077/1708] Change views menu title --- .../app/components/document-list/document-list.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index 0fd139b89..f408e1e2a 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -46,7 +46,7 @@ <div class="btn-group ml-2"> <div class="btn-group" ngbDropdown role="group"> - <button class="btn btn-sm btn-outline-primary dropdown-toggle" ngbDropdownToggle>Saved Views</button> + <button class="btn btn-sm btn-outline-primary dropdown-toggle" ngbDropdownToggle>Views</button> <div class="dropdown-menu shadow" ngbDropdownMenu> <ng-container *ngIf="!list.savedViewId"> <button ngbDropdownItem *ngFor="let config of savedViewConfigService.getConfigs()" (click)="loadViewConfig(config)">{{config.title}}</button> From 2de546fd5285a5cfc30b3c0393bb528e771f20bc Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sun, 13 Dec 2020 11:03:50 -0800 Subject: [PATCH 0078/1708] Fix tag / correspondent / document type toggling logic --- .../app/services/filter-editor-view.service.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src-ui/src/app/services/filter-editor-view.service.ts b/src-ui/src/app/services/filter-editor-view.service.ts index b436ecde4..bd25ec83b 100644 --- a/src-ui/src/app/services/filter-editor-view.service.ts +++ b/src-ui/src/app/services/filter-editor-view.service.ts @@ -103,18 +103,16 @@ export class FilterEditorViewService { private toggleFilterByItem(item: ObjectWithId, filterRuleTypeID: number) { let filterRules = this.filterRules let filterRuleType: FilterRuleType = FILTER_RULE_TYPES.find(t => t.id == filterRuleTypeID) - let existingRule = filterRules.find(rule => rule.type.id == filterRuleType.id) + let existingRules = filterRules.filter(rule => rule.type.id == filterRuleType.id) - if (existingRule && existingRule.value == item.id) { - filterRules.splice(filterRules.indexOf(existingRule), 1) - } else if (existingRule && filterRuleType.id == FILTER_HAS_TAG) { - filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == filterRuleType.id), value: item.id}) - } else if (existingRule && existingRule.value == item.id) { - return - } else if (existingRule) { - existingRule.value = item.id + if (existingRules && filterRuleType.id == FILTER_HAS_TAG) { + let existingItemRule = existingRules?.find(rule => rule.value == item.id) + if (existingItemRule) filterRules.splice(filterRules.indexOf(existingItemRule), 1) + else filterRules.push({type: filterRuleType, value: item.id}) + } else if (existingRules.length) { // Correspondents & DocumentTypes only one + filterRules.find(rule => rule.type.id == filterRuleType.id).value = item.id } else { - filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == filterRuleType.id), value: item.id}) + filterRules.push({type: filterRuleType, value: item.id}) } this.filterRules = filterRules From a61ea3555acde275a80a9de18f0e88a9d0e0d753 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sun, 13 Dec 2020 11:17:10 -0800 Subject: [PATCH 0079/1708] Ok now toggling logic is fixed --- src-ui/src/app/services/filter-editor-view.service.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src-ui/src/app/services/filter-editor-view.service.ts b/src-ui/src/app/services/filter-editor-view.service.ts index bd25ec83b..071a0d577 100644 --- a/src-ui/src/app/services/filter-editor-view.service.ts +++ b/src-ui/src/app/services/filter-editor-view.service.ts @@ -104,11 +104,12 @@ export class FilterEditorViewService { let filterRules = this.filterRules let filterRuleType: FilterRuleType = FILTER_RULE_TYPES.find(t => t.id == filterRuleTypeID) let existingRules = filterRules.filter(rule => rule.type.id == filterRuleType.id) + let existingItemRule = existingRules?.find(rule => rule.value == item.id) - if (existingRules && filterRuleType.id == FILTER_HAS_TAG) { - let existingItemRule = existingRules?.find(rule => rule.value == item.id) - if (existingItemRule) filterRules.splice(filterRules.indexOf(existingItemRule), 1) - else filterRules.push({type: filterRuleType, value: item.id}) + if (existingRules && existingItemRule) { + filterRules.splice(filterRules.indexOf(existingItemRule), 1) // if exact rule exists just remove + } else if (existingItemRule && filterRuleType.multi) { // e.g. tags can have multiple + filterRules.push({type: filterRuleType, value: item.id}) } else if (existingRules.length) { // Correspondents & DocumentTypes only one filterRules.find(rule => rule.type.id == filterRuleType.id).value = item.id } else { From bcdbc975d61e50f1a52feab4d7c2a8338b3195a0 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sun, 13 Dec 2020 11:20:28 -0800 Subject: [PATCH 0080/1708] Show filter has items selected --- .../filter-dropdown-date.component.html | 9 ++++++++- .../filter-dropdown/filter-dropdown.component.html | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html index 83d8c6455..a80e1c491 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html @@ -1,5 +1,12 @@ <div class="btn-group" ngbDropdown role="group"> - <button class="btn btn-outline-primary btn-sm" id="dropdown{{title}}" ngbDropdownToggle>{{title}}</button> + <button class="btn btn-outline-primary btn-sm" id="dropdown{{title}}" ngbDropdownToggle> + <ng-container *ngIf="_dateBefore || _dateAfter"> + <svg 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> + </ng-container> + {{title}} + </button> <div class="dropdown-menu date-filter shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> <div class="list-group list-group-flush"> <div class="list-group-item d-flex flex-column align-items-start"> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html index 8c48f0e2f..591530ae2 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html @@ -1,5 +1,12 @@ <div class="btn-group" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)"> - <button class="btn btn-outline-primary btn-sm" id="dropdown{{title}}" ngbDropdownToggle>{{title}}</button> + <button class="btn btn-outline-primary btn-sm" id="dropdown{{title}}" ngbDropdownToggle> + <ng-container *ngIf="itemsSelected?.length > 0"> + <svg 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> + </ng-container> + {{title}} + </button> <div class="dropdown-menu quick-filter shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> <div class="list-group list-group-flush"> <input class="list-group-item form-control form-control-sm" type="text" [(ngModel)]="filterText" placeholder="Filter {{title}}" (keyup.enter)="listFilterEnter()" #listFilterTextInput> From 1ddad84985f918fb97b745a406f51c00f4f0d9e4 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sun, 13 Dec 2020 11:23:21 -0800 Subject: [PATCH 0081/1708] Fix visual clearing of date field --- .../filter-dropdown-date.component.html | 2 +- .../filter-dropdown-date.component.ts | 22 +++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html index a80e1c491..0a3dc7057 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html @@ -1,6 +1,6 @@ <div class="btn-group" ngbDropdown role="group"> <button class="btn btn-outline-primary btn-sm" id="dropdown{{title}}" ngbDropdownToggle> - <ng-container *ngIf="_dateBefore || _dateAfter"> + <ng-container *ngIf="dateBefore || dateAfter"> <svg 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> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts index 206dbe2c3..41cf97bb9 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts @@ -38,20 +38,24 @@ export class FilterDropdownDateComponent { ngOnChanges(changes: SimpleChange) { // this is a hacky workaround perhaps because of https://github.com/angular/angular/issues/11097 - let dateString: string + let dateString: string = '' let dateAfterChange: SimpleChange = changes['dateAfter'] let dateBeforeChange: SimpleChange = changes['dateBefore'] - if (dateAfterChange && dateAfterChange.currentValue && this.dpAfter) { - let dateAfterDate = dateAfterChange.currentValue as NgbDateStruct + if (this.dpBefore && this.dpAfter) { let dpAfterElRef: ElementRef = this.dpAfter['_elRef'] - dateString = `${dateAfterDate.year}-${dateAfterDate.month.toString().padStart(2,'0')}-${dateAfterDate.day.toString().padStart(2,'0')}` - dpAfterElRef.nativeElement.value = dateString - } else if (dateBeforeChange && dateBeforeChange.currentValue && this.dpBefore) { - let dateBeforeDate = dateBeforeChange.currentValue as NgbDateStruct let dpBeforeElRef: ElementRef = this.dpBefore['_elRef'] - dateString = `${dateBeforeChange.currentValue.year}-${dateBeforeChange.currentValue.month.toString().padStart(2,'0')}-${dateBeforeChange.currentValue.day.toString().padStart(2,'0')}` - dpBeforeElRef.nativeElement.value = dateString + + if (dateAfterChange && dateAfterChange.currentValue) { + let dateAfterDate = dateAfterChange.currentValue as NgbDateStruct + dateString = `${dateAfterDate.year}-${dateAfterDate.month.toString().padStart(2,'0')}-${dateAfterDate.day.toString().padStart(2,'0')}` + } else if (dateBeforeChange && dateBeforeChange.currentValue) { + let dateBeforeDate = dateBeforeChange.currentValue as NgbDateStruct + dateString = `${dateBeforeChange.currentValue.year}-${dateBeforeChange.currentValue.month.toString().padStart(2,'0')}-${dateBeforeChange.currentValue.day.toString().padStart(2,'0')}` + } else { + dpAfterElRef.nativeElement.value = dateString + dpBeforeElRef.nativeElement.value = dateString + } } } From 3f719a21e0c777a849e2c406d97c8374c9caa1fc Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sun, 13 Dec 2020 11:29:12 -0800 Subject: [PATCH 0082/1708] Typo from merge --- src-ui/src/app/app.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index 4ce212763..f935b7701 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -97,7 +97,7 @@ import { MetadataCollapseComponent } from './components/document-detail/metadata WelcomeWidgetComponent, YesNoPipe, FileSizePipe, - FilterPipe + FilterPipe, DocumentTitlePipe, MetadataCollapseComponent ], From 3d8cd0f0d6b98ca5dbc9319ca64113d59c95ca5a Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sun, 13 Dec 2020 11:32:13 -0800 Subject: [PATCH 0083/1708] change tag selected marker to badge --- .../filter-dropdown/filter-dropdown.component.html | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html index 591530ae2..b2d82f3d0 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html @@ -1,9 +1,7 @@ <div class="btn-group" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)"> <button class="btn btn-outline-primary btn-sm" id="dropdown{{title}}" ngbDropdownToggle> <ng-container *ngIf="itemsSelected?.length > 0"> - <svg 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 class="badge bg-primary text-light rounded-pill ml-auto">{{itemsSelected?.length}}</div> </ng-container> {{title}} </button> From 7d212f6e80259190d69cdb8ae8deb03b3948dc17 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sun, 13 Dec 2020 11:33:57 -0800 Subject: [PATCH 0084/1708] Last time fixing the toggling logic, I hope =/ --- src-ui/src/app/services/filter-editor-view.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src-ui/src/app/services/filter-editor-view.service.ts b/src-ui/src/app/services/filter-editor-view.service.ts index 071a0d577..3a3b70e9a 100644 --- a/src-ui/src/app/services/filter-editor-view.service.ts +++ b/src-ui/src/app/services/filter-editor-view.service.ts @@ -108,9 +108,9 @@ export class FilterEditorViewService { if (existingRules && existingItemRule) { filterRules.splice(filterRules.indexOf(existingItemRule), 1) // if exact rule exists just remove - } else if (existingItemRule && filterRuleType.multi) { // e.g. tags can have multiple + } else if (existingRules.length > 0 && filterRuleType.multi) { // e.g. tags can have multiple filterRules.push({type: filterRuleType, value: item.id}) - } else if (existingRules.length) { // Correspondents & DocumentTypes only one + } else if (existingRules.length > 0) { // Correspondents & DocumentTypes only one filterRules.find(rule => rule.type.id == filterRuleType.id).value = item.id } else { filterRules.push({type: filterRuleType, value: item.id}) From 2c18f6268b20ac93098dec9f5fb3ba6428729464 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sun, 13 Dec 2020 11:34:58 -0800 Subject: [PATCH 0085/1708] Comment cleanup --- src-ui/src/app/services/filter-editor-view.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src-ui/src/app/services/filter-editor-view.service.ts b/src-ui/src/app/services/filter-editor-view.service.ts index 3a3b70e9a..ee7961f0f 100644 --- a/src-ui/src/app/services/filter-editor-view.service.ts +++ b/src-ui/src/app/services/filter-editor-view.service.ts @@ -106,11 +106,11 @@ export class FilterEditorViewService { let existingRules = filterRules.filter(rule => rule.type.id == filterRuleType.id) let existingItemRule = existingRules?.find(rule => rule.value == item.id) - if (existingRules && existingItemRule) { - filterRules.splice(filterRules.indexOf(existingItemRule), 1) // if exact rule exists just remove + if (existingRules && existingItemRule) { // if exact rule exists just remove + filterRules.splice(filterRules.indexOf(existingItemRule), 1) } else if (existingRules.length > 0 && filterRuleType.multi) { // e.g. tags can have multiple filterRules.push({type: filterRuleType, value: item.id}) - } else if (existingRules.length > 0) { // Correspondents & DocumentTypes only one + } else if (existingRules.length > 0) { // correspondents & documentTypes can only be one filterRules.find(rule => rule.type.id == filterRuleType.id).value = item.id } else { filterRules.push({type: filterRuleType, value: item.id}) From 7ac101d84ee26620cb7a3928b2b8b5daf68eb5ce Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sun, 13 Dec 2020 12:17:01 -0800 Subject: [PATCH 0086/1708] Typo! --- .../document-list/document-list.component.ts | 6 +++--- .../filter-editor/filter-editor.component.ts | 6 +++--- .../app/services/filter-editor-view.service.ts | 18 +++++++++--------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index fe03cae80..cd60054c7 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -113,17 +113,17 @@ export class DocumentListComponent implements OnInit { } clickTag(tagID: number) { - this.filterEditorService.toggleFitlerByTagID(tagID) + this.filterEditorService.toggleFilterByTagID(tagID) this.applyFilterRules() } clickCorrespondent(correspondentID: number) { - this.filterEditorService.toggleFitlerByCorrespondentID(correspondentID) + this.filterEditorService.toggleFilterByCorrespondentID(correspondentID) this.applyFilterRules() } clickDocumentType(documentTypeID: number) { - this.filterEditorService.toggleFitlerByDocumentTypeID(documentTypeID) + this.filterEditorService.toggleFilterByDocumentTypeID(documentTypeID) this.applyFilterRules() } diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/filter-editor/filter-editor.component.ts index bb838f0be..a6940795e 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.ts @@ -52,17 +52,17 @@ export class FilterEditorComponent implements AfterViewInit { } onToggleTag(tag: PaperlessTag) { - this.filterEditorService.toggleFitlerByTag(tag) + this.filterEditorService.toggleFilterByTag(tag) this.applyFilters() } onToggleCorrespondent(correspondent: PaperlessCorrespondent) { - this.filterEditorService.toggleFitlerByCorrespondent(correspondent) + this.filterEditorService.toggleFilterByCorrespondent(correspondent) this.applyFilters() } onToggleDocumentType(documentType: PaperlessDocumentType) { - this.filterEditorService.toggleFitlerByDocumentType(documentType) + this.filterEditorService.toggleFilterByDocumentType(documentType) this.applyFilters() } diff --git a/src-ui/src/app/services/filter-editor-view.service.ts b/src-ui/src/app/services/filter-editor-view.service.ts index ee7961f0f..0893fff2e 100644 --- a/src-ui/src/app/services/filter-editor-view.service.ts +++ b/src-ui/src/app/services/filter-editor-view.service.ts @@ -76,28 +76,28 @@ export class FilterEditorViewService { return this.documentTypes?.filter(dt => documentTypeRules.find(dtr => dtr.value == dt.id)) } - toggleFitlerByTag(tag: PaperlessTag) { + toggleFilterByTag(tag: PaperlessTag) { this.toggleFilterByItem(tag, FILTER_HAS_TAG) } - toggleFitlerByCorrespondent(tag: PaperlessCorrespondent) { + toggleFilterByCorrespondent(tag: PaperlessCorrespondent) { this.toggleFilterByItem(tag, FILTER_CORRESPONDENT) } - toggleFitlerByDocumentType(tag: PaperlessDocumentType) { + toggleFilterByDocumentType(tag: PaperlessDocumentType) { this.toggleFilterByItem(tag, FILTER_DOCUMENT_TYPE) } - toggleFitlerByTagID(tagID: number) { - this.toggleFitlerByTag(this.tags?.find(t => t.id == tagID)) + toggleFilterByTagID(tagID: number) { + this.toggleFilterByTag(this.tags?.find(t => t.id == tagID)) } - toggleFitlerByCorrespondentID(correspondentID: number) { - this.toggleFitlerByCorrespondent(this.correspondents?.find(t => t.id == correspondentID)) + toggleFilterByCorrespondentID(correspondentID: number) { + this.toggleFilterByCorrespondent(this.correspondents?.find(t => t.id == correspondentID)) } - toggleFitlerByDocumentTypeID(documentTypeID: number) { - this.toggleFitlerByDocumentType(this.documentTypes?.find(t => t.id == documentTypeID)) + toggleFilterByDocumentTypeID(documentTypeID: number) { + this.toggleFilterByDocumentType(this.documentTypes?.find(t => t.id == documentTypeID)) } private toggleFilterByItem(item: ObjectWithId, filterRuleTypeID: number) { From ae51619243b9019902f05a6da26a54a4652a6329 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sun, 13 Dec 2020 14:11:43 -0800 Subject: [PATCH 0087/1708] Make date buttons same as other dropdowns --- .../filter-dropdown-date.component.html | 11 +++++------ .../filter-dropdown-date.component.ts | 4 ++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html index 0a3dc7057..94da65d7c 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html @@ -9,12 +9,11 @@ </button> <div class="dropdown-menu date-filter shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> <div class="list-group list-group-flush"> - <div class="list-group-item d-flex flex-column align-items-start"> - <button class="btn btn-sm btn-link pl-0" (click)="setDateQuickFilter(7)">Last 7 days</button> - <button class="btn btn-sm btn-link pl-0" (click)="setDateQuickFilter(30)">Last 30 days</button> - <button class="btn btn-sm btn-link pl-0" (click)="setDateQuickFilter('month')">This month</button> - <button class="btn btn-sm btn-link pl-0" (click)="setDateQuickFilter('year')">This year</button> - </div> + <button *ngFor="let range of [7, 30, 'month', 'year']" class="list-group-item small list-goup list-group-item-action d-flex" role="menuitem" (click)="setDateQuickFilter(range)"> + <ng-container *ngIf="isStringRange(range)">This </ng-container> + {{ range }} + <ng-container *ngIf="!isStringRange(range)"> days</ng-container> + </button> <div class="list-group-item d-flex flex-column align-items-start" role="menuitem"> <div class="mb-1"><small>Before</small></div> <div class="input-group input-group-sm"> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts index 41cf97bb9..3a85547ea 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts @@ -36,6 +36,10 @@ export class FilterDropdownDateComponent { return NgbDate.from({year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate()}) } + isStringRange(range: any) { + return typeof range == 'string' + } + ngOnChanges(changes: SimpleChange) { // this is a hacky workaround perhaps because of https://github.com/angular/angular/issues/11097 let dateString: string = '' From 1fafb9ace6af9da4a977f78dc88cd3f1a6680ca9 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sun, 13 Dec 2020 14:40:17 -0800 Subject: [PATCH 0088/1708] Prettier styling on dropdowns --- .../filter-dropdown-date.component.html | 4 +-- .../filter-dropdown-button.component.html | 4 +-- .../filter-dropdown.component.html | 28 ++++++++++++++----- .../filter-dropdown.component.scss | 9 ++++-- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html index 94da65d7c..e41f7c7ab 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html @@ -7,9 +7,9 @@ </ng-container> {{title}} </button> - <div class="dropdown-menu date-filter shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> + <div class="dropdown-menu date-filter shadow pt-0" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> <div class="list-group list-group-flush"> - <button *ngFor="let range of [7, 30, 'month', 'year']" class="list-group-item small list-goup list-group-item-action d-flex" role="menuitem" (click)="setDateQuickFilter(range)"> + <button *ngFor="let range of [7, 30, 'month', 'year']" class="list-group-item small list-goup list-group-item-action d-flex p-2 pl-3" role="menuitem" (click)="setDateQuickFilter(range)"> <ng-container *ngIf="isStringRange(range)">This </ng-container> {{ range }} <ng-container *ngIf="!isStringRange(range)"> days</ng-container> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.html index 10068c675..eef4e2c17 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.html @@ -1,4 +1,4 @@ -<button class="list-group-item list-group-item-action d-flex align-items-center" role="menuitem" (click)="toggleItem()"> +<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"/> @@ -8,5 +8,5 @@ <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 bg-primary text-light rounded-pill ml-auto">{{item.document_count}}</div> + <div class="badge badge-light rounded-pill ml-auto">{{item.document_count}}</div> </button> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html index b2d82f3d0..99ab629dc 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html @@ -1,18 +1,32 @@ <div class="btn-group" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)"> <button class="btn btn-outline-primary btn-sm" id="dropdown{{title}}" ngbDropdownToggle> - <ng-container *ngIf="itemsSelected?.length > 0"> - <div class="badge bg-primary text-light rounded-pill ml-auto">{{itemsSelected?.length}}</div> - </ng-container> + <div class="badge bg-primary text-light rounded-pill ml-auto"> + <ng-container *ngIf="itemsSelected?.length > 0"> + {{itemsSelected?.length}} + </ng-container> + </div> {{title}} </button> - <div class="dropdown-menu quick-filter shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> + <div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> <div class="list-group list-group-flush"> - <input class="list-group-item form-control form-control-sm" type="text" [(ngModel)]="filterText" placeholder="Filter {{title}}" (keyup.enter)="listFilterEnter()" #listFilterTextInput> - <ng-container *ngIf="items"> + <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 class="input-group-append"> + <span class="input-group-text bg-light text-muted"> + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-search" viewBox="0 0 16 16"> + <path fill-rule="evenodd" d="M10.442 10.442a1 1 0 0 1 1.415 0l3.85 3.85a1 1 0 0 1-1.414 1.415l-3.85-3.85a1 1 0 0 1 0-1.415z"/> + <path fill-rule="evenodd" d="M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0z"/> + </svg> + </span> + </div> + </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> - </ng-container> + </div> </div> </div> </div> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.scss b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.scss index 5551b0329..d34729eee 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.scss +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.scss @@ -1,5 +1,8 @@ -.quick-filter { +.dropdown-menu { min-width: 250px; - max-height: 400px; - overflow-y: scroll; + + .items { + max-height: 400px; + overflow-y: scroll; + } } From 04bb7d48935d034792c12a4fb98e4ca906f1dd2a Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sun, 13 Dec 2020 14:46:10 -0800 Subject: [PATCH 0089/1708] Remove card around filter editor --- .../components/document-list/document-list.component.html | 6 ++---- .../filter-dropdown/filter-dropdown.component.html | 6 +++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index ed54dc3fb..b1b2c8f94 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -62,10 +62,8 @@ </app-page-header> -<div class="card w-100 mb-3"> - <div class="card-body"> - <app-filter-editor [(filterEditorService)]="filterEditorService" (apply)="applyFilterRules()" (clear)="clearFilterRules()" #filterEditor></app-filter-editor> - </div> +<div class="w-100 mb-4"> + <app-filter-editor [(filterEditorService)]="filterEditorService" (apply)="applyFilterRules()" (clear)="clearFilterRules()" #filterEditor></app-filter-editor> </div> <div class="d-flex justify-content-between align-items-center"> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html index 99ab629dc..523dd084a 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html @@ -1,10 +1,10 @@ <div class="btn-group" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)"> <button class="btn btn-outline-primary btn-sm" id="dropdown{{title}}" ngbDropdownToggle> - <div class="badge bg-primary text-light rounded-pill ml-auto"> - <ng-container *ngIf="itemsSelected?.length > 0"> + <ng-container *ngIf="itemsSelected?.length > 0"> + <div class="badge bg-secondary text-light rounded-pill ml-auto"> {{itemsSelected?.length}} - </ng-container> </div> + </ng-container> {{title}} </button> <div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> From 89cb1211e74bfe8e2b013f5c860c0efcbbcca2bc Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Sun, 13 Dec 2020 23:46:48 +0100 Subject: [PATCH 0090/1708] docs --- docs/faq.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/faq.rst b/docs/faq.rst index 6eac18617..d9efddd0f 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -78,6 +78,12 @@ that automatically, I'm all ears. For now, you have to grab the latest release archive from the project page and build the image yourself. The release comes with the front end already compiled, so you don't have to do this on the Pi. +**Q:** *How do I run this on unRaid?* + +**A:** Head over to `<https://github.com/selfhosters/unRAID-CA-templates>`_, +`Uli Fahrer <https://github.com/Tooa>`_ created a container template for that. +I don't exactly know how to use that though, since I don't use unRaid. + **Q:** *How do I run this on my toaster?* **A:** I honestly don't know! As for all other devices that might be able From 8e5c2a2b145383203d2fa681c160bec081989673 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sun, 13 Dec 2020 15:08:45 -0800 Subject: [PATCH 0091/1708] Fix date clearing --- .../filter-dropdown-date/filter-dropdown-date.component.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts index 3a85547ea..e5f9675d1 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts @@ -53,9 +53,11 @@ export class FilterDropdownDateComponent { if (dateAfterChange && dateAfterChange.currentValue) { let dateAfterDate = dateAfterChange.currentValue as NgbDateStruct dateString = `${dateAfterDate.year}-${dateAfterDate.month.toString().padStart(2,'0')}-${dateAfterDate.day.toString().padStart(2,'0')}` + dpAfterElRef.nativeElement.value = dateString } else if (dateBeforeChange && dateBeforeChange.currentValue) { let dateBeforeDate = dateBeforeChange.currentValue as NgbDateStruct dateString = `${dateBeforeChange.currentValue.year}-${dateBeforeChange.currentValue.month.toString().padStart(2,'0')}-${dateBeforeChange.currentValue.day.toString().padStart(2,'0')}` + dpBeforeElRef.nativeElement.value = dateString } else { dpAfterElRef.nativeElement.value = dateString dpBeforeElRef.nativeElement.value = dateString From ee7492cf52e05c450b64b226aff5392fd4ac69c1 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sun, 13 Dec 2020 15:38:28 -0800 Subject: [PATCH 0092/1708] Clear date filter buttons --- .../filter-dropdown-date.component.html | 22 ++++++++++++-- .../filter-dropdown-date.component.scss | 4 +++ .../filter-dropdown-date.component.ts | 16 ++++++++-- .../services/filter-editor-view.service.ts | 29 +++++++++++++------ 4 files changed, 57 insertions(+), 14 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html index e41f7c7ab..7b83f6619 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html @@ -15,7 +15,15 @@ <ng-container *ngIf="!isStringRange(range)"> days</ng-container> </button> <div class="list-group-item d-flex flex-column align-items-start" role="menuitem"> - <div class="mb-1"><small>Before</small></div> + <div class="mb-2 d-flex flex-row w-100 justify-content-between small"> + <div>Before</div> + <a *ngIf="dateBefore" class="btn btn-link p-0 m-0" (click)="clearBefore()"> + <svg width="0.8em" height="0.8em" viewBox="0 0 16 16" class="bi bi-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> + <path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/> + </svg> + <small>Clear</small> + </a> + </div> <div class="input-group input-group-sm"> <input class="form-control" type="text" placeholder="yyyy-mm-dd" name="before" [(ngModel)]="_dateBefore" [maxDate]="this._maxDate" ngbDatepicker (dateSelect)="onDateSelected($event)" #dpBefore="ngbDatepicker"> <div class="input-group-append"> @@ -29,8 +37,16 @@ </div> </div> <div class="list-group-item d-flex flex-column align-items-start" role="menuitem"> - <div class="mb-1"><small>After</small></div> - <div class="input-group"> + <div class="mb-2 d-flex flex-row w-100 justify-content-between small"> + <div>After</div> + <a *ngIf="dateAfter" class="btn btn-link p-0 m-0" (click)="clearAfter()"> + <svg width="0.8em" height="0.8em" viewBox="0 0 16 16" class="bi bi-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> + <path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/> + </svg> + <small>Clear</small> + </a> + </div> + <div class="input-group input-group-sm"> <input class="form-control form-control-sm" type="text" placeholder="yyyy-mm-dd" name="after" [(ngModel)]="_dateAfter" [maxDate]="this._maxDate" ngbDatepicker (dateSelect)="onDateSelected($event)" #dpAfter="ngbDatepicker"> <div class="input-group-append"> <button class="btn btn-outline-secondary btn-sm" (click)="dpAfter.toggle()" type="button"> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.scss b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.scss index 67edb9bf8..3bdedd8a0 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.scss +++ b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.scss @@ -1,3 +1,7 @@ .date-filter { min-width: 250px; + + .btn-link { + line-height: 1; + } } diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts index e5f9675d1..f69028bf0 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts @@ -43,8 +43,12 @@ export class FilterDropdownDateComponent { ngOnChanges(changes: SimpleChange) { // this is a hacky workaround perhaps because of https://github.com/angular/angular/issues/11097 let dateString: string = '' - let dateAfterChange: SimpleChange = changes['dateAfter'] - let dateBeforeChange: SimpleChange = changes['dateBefore'] + let dateAfterChange: SimpleChange + let dateBeforeChange: SimpleChange + if (changes) { + dateAfterChange = changes['dateAfter'] + dateBeforeChange = changes['dateBefore'] + } if (this.dpBefore && this.dpAfter) { let dpAfterElRef: ElementRef = this.dpAfter['_elRef'] @@ -93,4 +97,12 @@ export class FilterDropdownDateComponent { let emitter = this._dateAfter && NgbDate.from(this._dateAfter).equals(date) ? this.dateAfterSet : this.dateBeforeSet emitter.emit(date) } + + clearAfter() { + this.dateAfterSet.next() + } + + clearBefore() { + this.dateBeforeSet.next() + } } diff --git a/src-ui/src/app/services/filter-editor-view.service.ts b/src-ui/src/app/services/filter-editor-view.service.ts index 0893fff2e..b50391376 100644 --- a/src-ui/src/app/services/filter-editor-view.service.ts +++ b/src-ui/src/app/services/filter-editor-view.service.ts @@ -155,23 +155,27 @@ export class FilterEditorViewService { } : undefined } - setDateCreatedBefore(date: NgbDateStruct) { - this.setDate(date, FILTER_CREATED_BEFORE) + setDateCreatedBefore(date?: NgbDateStruct) { + if (date) this.setDateFilter(date, FILTER_CREATED_BEFORE) + else this.clearDateFilter(FILTER_CREATED_BEFORE) } - setDateCreatedAfter(date: NgbDateStruct) { - this.setDate(date, FILTER_CREATED_AFTER) + setDateCreatedAfter(date?: NgbDateStruct) { + if (date) this.setDateFilter(date, FILTER_CREATED_AFTER) + else this.clearDateFilter(FILTER_CREATED_AFTER) } - setDateAddedBefore(date: NgbDateStruct) { - this.setDate(date, FILTER_ADDED_BEFORE) + setDateAddedBefore(date?: NgbDateStruct) { + if (date) this.setDateFilter(date, FILTER_ADDED_BEFORE) + else this.clearDateFilter(FILTER_ADDED_BEFORE) } - setDateAddedAfter(date: NgbDateStruct) { - this.setDate(date, FILTER_ADDED_AFTER) + setDateAddedAfter(date?: NgbDateStruct) { + if (date) this.setDateFilter(date, FILTER_ADDED_AFTER) + else this.clearDateFilter(FILTER_ADDED_AFTER) } - setDate(date: NgbDateStruct, dateRuleTypeID: number) { + setDateFilter(date: NgbDateStruct, dateRuleTypeID: number) { let filterRules = this.filterRules let existingRule = filterRules.find(rule => rule.type.id == dateRuleTypeID) let newValue = `${date.year}-${date.month.toString().padStart(2,'0')}-${date.day.toString().padStart(2,'0')}` // YYYY-MM-DD @@ -184,4 +188,11 @@ export class FilterEditorViewService { this.filterRules = filterRules } + + clearDateFilter(dateRuleTypeID: number) { + let filterRules = this.filterRules + let existingRule = filterRules.find(rule => rule.type.id == dateRuleTypeID) + filterRules.splice(filterRules.indexOf(existingRule), 1) + this.filterRules = filterRules + } } From 245af658419aa5404a4cbba840c2074de2dddccd Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sun, 13 Dec 2020 15:47:46 -0800 Subject: [PATCH 0093/1708] Auto-close menu when single item chosen with Enter key --- .../filter-dropdown/filter-dropdown.component.html | 2 +- .../filter-editor/filter-dropdown/filter-dropdown.component.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html index 523dd084a..0a3fe5496 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html @@ -1,4 +1,4 @@ - <div class="btn-group" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)"> +<div class="btn-group" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #filterDropdown="ngbDropdown"> <button class="btn btn-outline-primary btn-sm" id="dropdown{{title}}" ngbDropdownToggle> <ng-container *ngIf="itemsSelected?.length > 0"> <div class="badge bg-secondary text-light rounded-pill ml-auto"> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts index 720c86a94..d8f9b78c9 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts @@ -29,6 +29,7 @@ export class FilterDropdownComponent { toggle = new EventEmitter() @ViewChild('listFilterTextInput') listFilterTextInput: ElementRef + @ViewChild('filterDropdown') filterDropdown: NgbDropdown filterText: string @@ -53,5 +54,6 @@ export class FilterDropdownComponent { listFilterEnter(): void { let filtered = this.filterPipe.transform(this.items, this.filterText) if (filtered.length == 1) this.toggleItem(filtered.shift()) + this.filterDropdown.close() } } From 251fc582e93f30dae7dd3fc5fe83b93c5e863fa6 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Mon, 14 Dec 2020 02:19:53 +0100 Subject: [PATCH 0094/1708] fixes #130 --- src/documents/file_handling.py | 6 +++++- src/documents/tests/test_file_handling.py | 22 +++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/documents/file_handling.py b/src/documents/file_handling.py index c5efc33e4..d28f9ffbb 100644 --- a/src/documents/file_handling.py +++ b/src/documents/file_handling.py @@ -121,7 +121,11 @@ def generate_filename(doc, counter=0): added_month=doc.added.month if doc.added else "none", added_day=doc.added.day if doc.added else "none", tags=tags, - ) + tag_list=",".join([tag.name for tag in doc.tags.all()]) + ).strip() + + path = path.strip(os.sep) + except (ValueError, KeyError, IndexError): logging.getLogger(__name__).warning( f"Invalid PAPERLESS_FILENAME_FORMAT: " diff --git a/src/documents/tests/test_file_handling.py b/src/documents/tests/test_file_handling.py index 719b0078a..2b1022453 100644 --- a/src/documents/tests/test_file_handling.py +++ b/src/documents/tests/test_file_handling.py @@ -13,7 +13,7 @@ from django.test import TestCase, override_settings from .utils import DirectoriesMixin from ..file_handling import generate_filename, create_source_path_directory, delete_empty_directories, \ generate_unique_filename -from ..models import Document, Correspondent +from ..models import Document, Correspondent, Tag class TestFileHandling(DirectoriesMixin, TestCase): @@ -267,6 +267,26 @@ class TestFileHandling(DirectoriesMixin, TestCase): self.assertEqual(generate_filename(document), "none.pdf") + @override_settings(PAPERLESS_FILENAME_FORMAT="{title} {tag_list}") + def test_tag_list(self): + doc = Document.objects.create(title="doc1", mime_type="application/pdf") + doc.tags.create(name="tag2") + doc.tags.create(name="tag1") + + self.assertEqual(generate_filename(doc), "doc1 tag1,tag2.pdf") + + doc = Document.objects.create(title="doc2", checksum="B", mime_type="application/pdf") + + self.assertEqual(generate_filename(doc), "doc2.pdf") + + @override_settings(PAPERLESS_FILENAME_FORMAT="//etc/something/{title}") + def test_filename_relative(self): + doc = Document.objects.create(title="doc1", mime_type="application/pdf") + doc.filename = generate_filename(doc) + doc.save() + + self.assertEqual(doc.source_path, os.path.join(settings.ORIGINALS_DIR, "etc", "something", "doc1.pdf")) + @override_settings(PAPERLESS_FILENAME_FORMAT="{correspondent}/{correspondent}/{correspondent}") def test_nested_directory_cleanup(self): document = Document() From bad7caa8b9cfc9a98f1caf191422a74892148f67 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Mon, 14 Dec 2020 02:46:46 +0100 Subject: [PATCH 0095/1708] fixes #117 --- src/documents/file_handling.py | 8 ++++---- src/documents/tests/test_file_handling.py | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/documents/file_handling.py b/src/documents/file_handling.py index d28f9ffbb..861eb2a37 100644 --- a/src/documents/file_handling.py +++ b/src/documents/file_handling.py @@ -114,12 +114,12 @@ def generate_filename(doc, counter=0): document_type=document_type, created=datetime.date.isoformat(doc.created), created_year=doc.created.year if doc.created else "none", - created_month=doc.created.month if doc.created else "none", - created_day=doc.created.day if doc.created else "none", + created_month=f"{doc.created.month:02}" if doc.created else "none", # NOQA: E501 + created_day=f"{doc.created.day:02}" if doc.created else "none", added=datetime.date.isoformat(doc.added), added_year=doc.added.year if doc.added else "none", - added_month=doc.added.month if doc.added else "none", - added_day=doc.added.day if doc.added else "none", + added_month=f"{doc.added.month:02}" if doc.added else "none", + added_day=f"{doc.added.day:02}" if doc.added else "none", tags=tags, tag_list=",".join([tag.name for tag in doc.tags.all()]) ).strip() diff --git a/src/documents/tests/test_file_handling.py b/src/documents/tests/test_file_handling.py index 2b1022453..2f7f6efcf 100644 --- a/src/documents/tests/test_file_handling.py +++ b/src/documents/tests/test_file_handling.py @@ -287,6 +287,28 @@ class TestFileHandling(DirectoriesMixin, TestCase): self.assertEqual(doc.source_path, os.path.join(settings.ORIGINALS_DIR, "etc", "something", "doc1.pdf")) + @override_settings(PAPERLESS_FILENAME_FORMAT="{created_year}-{created_month}-{created_day}") + def test_created_year_month_day(self): + d1 = datetime.datetime(2020, 3, 6, 1, 1, 1) + doc1 = Document.objects.create(title="doc1", mime_type="application/pdf", created=d1) + + self.assertEqual(generate_filename(doc1), "2020-03-06.pdf") + + doc1.created = datetime.datetime(2020, 11, 16, 1, 1, 1) + + self.assertEqual(generate_filename(doc1), "2020-11-16.pdf") + + @override_settings(PAPERLESS_FILENAME_FORMAT="{added_year}-{added_month}-{added_day}") + def test_added_year_month_day(self): + d1 = datetime.datetime(232, 1, 9, 1, 1, 1) + doc1 = Document.objects.create(title="doc1", mime_type="application/pdf", added=d1) + + self.assertEqual(generate_filename(doc1), "232-01-09.pdf") + + doc1.added = datetime.datetime(2020, 11, 16, 1, 1, 1) + + self.assertEqual(generate_filename(doc1), "2020-11-16.pdf") + @override_settings(PAPERLESS_FILENAME_FORMAT="{correspondent}/{correspondent}/{correspondent}") def test_nested_directory_cleanup(self): document = Document() From 8cc03363381231c2f6d8f45d11da1f78f461b483 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Mon, 14 Dec 2020 03:01:50 +0100 Subject: [PATCH 0096/1708] prevent usage of {tags} directly. --- src/documents/file_handling.py | 10 ++++++++-- src/documents/tests/test_file_handling.py | 9 +++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/documents/file_handling.py b/src/documents/file_handling.py index 861eb2a37..c49493991 100644 --- a/src/documents/file_handling.py +++ b/src/documents/file_handling.py @@ -8,6 +8,12 @@ from django.conf import settings from django.template.defaultfilters import slugify +class defaultdictNoStr(defaultdict): + + def __str__(self): + raise ValueError("Don't use {tags} directly.") + + def create_source_path_directory(source_path): os.makedirs(os.path.dirname(source_path), exist_ok=True) @@ -90,8 +96,8 @@ def generate_filename(doc, counter=0): try: if settings.PAPERLESS_FILENAME_FORMAT is not None: - tags = defaultdict(lambda: slugify(None), - many_to_dictionary(doc.tags)) + tags = defaultdictNoStr(lambda: slugify(None), + many_to_dictionary(doc.tags)) if doc.correspondent: correspondent = pathvalidate.sanitize_filename( diff --git a/src/documents/tests/test_file_handling.py b/src/documents/tests/test_file_handling.py index 2f7f6efcf..dec89c45b 100644 --- a/src/documents/tests/test_file_handling.py +++ b/src/documents/tests/test_file_handling.py @@ -267,6 +267,15 @@ class TestFileHandling(DirectoriesMixin, TestCase): self.assertEqual(generate_filename(document), "none.pdf") + @override_settings(PAPERLESS_FILENAME_FORMAT="{tags}") + def test_tags_without_args(self): + document = Document() + document.mime_type = "application/pdf" + document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() + + self.assertEqual(generate_filename(document), f"{document.pk:07}.pdf") + @override_settings(PAPERLESS_FILENAME_FORMAT="{title} {tag_list}") def test_tag_list(self): doc = Document.objects.create(title="doc1", mime_type="application/pdf") From a12ec00827db9bc67414726665e350c3f00f6600 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sun, 13 Dec 2020 20:16:15 -0800 Subject: [PATCH 0097/1708] Remove unused displayName --- src-ui/src/app/data/filter-rule-type.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src-ui/src/app/data/filter-rule-type.ts b/src-ui/src/app/data/filter-rule-type.ts index cf155daf1..ea8e60eee 100644 --- a/src-ui/src/app/data/filter-rule-type.ts +++ b/src-ui/src/app/data/filter-rule-type.ts @@ -25,23 +25,23 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [ {id: FILTER_ASN, name: "ASN is", filtervar: "archive_serial_number", datatype: "number", multi: false}, - {id: FILTER_CORRESPONDENT, name: "Correspondent is", displayName: "Correspondents", filtervar: "correspondent__id", datatype: "correspondent", multi: false}, - {id: FILTER_DOCUMENT_TYPE, name: "Document type is", displayName: "Document types", filtervar: "document_type__id", datatype: "document_type", multi: false}, + {id: FILTER_CORRESPONDENT, name: "Correspondent is", filtervar: "correspondent__id", datatype: "correspondent", multi: false}, + {id: FILTER_DOCUMENT_TYPE, name: "Document type is", filtervar: "document_type__id", datatype: "document_type", multi: false}, {id: FILTER_IS_IN_INBOX, name: "Is in Inbox", filtervar: "is_in_inbox", datatype: "boolean", multi: false, default: true}, - {id: FILTER_HAS_TAG, name: "Has tag", displayName: "Tags", filtervar: "tags__id__all", datatype: "tag", multi: true}, + {id: FILTER_HAS_TAG, name: "Has tag", filtervar: "tags__id__all", datatype: "tag", multi: true}, {id: FILTER_DOES_NOT_HAVE_TAG, name: "Does not have tag", filtervar: "tags__id__none", datatype: "tag", multi: true}, {id: FILTER_HAS_ANY_TAG, name: "Has any tag", filtervar: "is_tagged", datatype: "boolean", multi: false, default: true}, - {id: FILTER_CREATED_BEFORE, name: "Created before", displayName: "Created", filtervar: "created__date__lt", datatype: "date", multi: false}, - {id: FILTER_CREATED_AFTER, name: "Created after", displayName: "Created", filtervar: "created__date__gt", datatype: "date", multi: false}, + {id: FILTER_CREATED_BEFORE, name: "Created before", filtervar: "created__date__lt", datatype: "date", multi: false}, + {id: FILTER_CREATED_AFTER, name: "Created after", filtervar: "created__date__gt", datatype: "date", multi: false}, {id: FILTER_CREATED_YEAR, name: "Year created is", filtervar: "created__year", datatype: "number", multi: false}, {id: FILTER_CREATED_MONTH, name: "Month created is", filtervar: "created__month", datatype: "number", multi: false}, {id: FILTER_CREATED_DAY, name: "Day created is", filtervar: "created__day", datatype: "number", multi: false}, - {id: FILTER_ADDED_BEFORE, name: "Added before", displayName: "Added", filtervar: "added__date__lt", datatype: "date", multi: false}, - {id: FILTER_ADDED_AFTER, name: "Added after", displayName: "Added", filtervar: "added__date__gt", datatype: "date", multi: false}, + {id: FILTER_ADDED_BEFORE, name: "Added before", filtervar: "added__date__lt", datatype: "date", multi: false}, + {id: FILTER_ADDED_AFTER, name: "Added after", filtervar: "added__date__gt", datatype: "date", multi: false}, {id: FILTER_MODIFIED_BEFORE, name: "Modified before", filtervar: "modified__date__lt", datatype: "date", multi: false}, {id: FILTER_MODIFIED_AFTER, name: "Modified after", filtervar: "modified__date__gt", datatype: "date", multi: false}, @@ -53,6 +53,5 @@ export interface FilterRuleType { filtervar: string datatype: string //number, string, boolean, date multi: boolean - displayName?: string default?: any } From 9bfc92cf79872d5d0f31e1dca36618f515873fda Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sun, 13 Dec 2020 20:16:23 -0800 Subject: [PATCH 0098/1708] Fix missing NgbDropdown import --- .../filter-editor/filter-dropdown/filter-dropdown.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts index d8f9b78c9..a24e7347d 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts @@ -3,6 +3,7 @@ import { Observable } from 'rxjs'; import { Results } from 'src/app/data/results'; 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', From 54d90a4c4b8215551546447073c662e4df0cef63 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sun, 13 Dec 2020 20:37:13 -0800 Subject: [PATCH 0099/1708] Code cleanup --- .../document-list/document-list.component.ts | 6 ++--- .../services/filter-editor-view.service.ts | 25 ++++++------------- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index cd60054c7..8d090f001 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -113,17 +113,17 @@ export class DocumentListComponent implements OnInit { } clickTag(tagID: number) { - this.filterEditorService.toggleFilterByTagID(tagID) + this.filterEditorService.toggleFilterByTag(tagID) this.applyFilterRules() } clickCorrespondent(correspondentID: number) { - this.filterEditorService.toggleFilterByCorrespondentID(correspondentID) + this.filterEditorService.toggleFilterByCorrespondent(correspondentID) this.applyFilterRules() } clickDocumentType(documentTypeID: number) { - this.filterEditorService.toggleFilterByDocumentTypeID(documentTypeID) + this.filterEditorService.toggleFilterByDocumentType(documentTypeID) this.applyFilterRules() } diff --git a/src-ui/src/app/services/filter-editor-view.service.ts b/src-ui/src/app/services/filter-editor-view.service.ts index b50391376..89f40189c 100644 --- a/src-ui/src/app/services/filter-editor-view.service.ts +++ b/src-ui/src/app/services/filter-editor-view.service.ts @@ -76,28 +76,19 @@ export class FilterEditorViewService { return this.documentTypes?.filter(dt => documentTypeRules.find(dtr => dtr.value == dt.id)) } - toggleFilterByTag(tag: PaperlessTag) { + toggleFilterByTag(tag: PaperlessTag | number) { + if (typeof tag == 'number') tag = this.tags?.find(t => t.id == tag) this.toggleFilterByItem(tag, FILTER_HAS_TAG) } - toggleFilterByCorrespondent(tag: PaperlessCorrespondent) { - this.toggleFilterByItem(tag, FILTER_CORRESPONDENT) + toggleFilterByCorrespondent(correspondent: PaperlessCorrespondent | number) { + if (typeof correspondent == 'number') correspondent = this.correspondents?.find(t => t.id == correspondent) + this.toggleFilterByItem(correspondent, FILTER_CORRESPONDENT) } - toggleFilterByDocumentType(tag: PaperlessDocumentType) { - this.toggleFilterByItem(tag, FILTER_DOCUMENT_TYPE) - } - - toggleFilterByTagID(tagID: number) { - this.toggleFilterByTag(this.tags?.find(t => t.id == tagID)) - } - - toggleFilterByCorrespondentID(correspondentID: number) { - this.toggleFilterByCorrespondent(this.correspondents?.find(t => t.id == correspondentID)) - } - - toggleFilterByDocumentTypeID(documentTypeID: number) { - this.toggleFilterByDocumentType(this.documentTypes?.find(t => t.id == documentTypeID)) + toggleFilterByDocumentType(documentType: PaperlessDocumentType | number) { + if (typeof documentType == 'number') documentType = this.documentTypes?.find(t => t.id == documentType) + this.toggleFilterByItem(documentType, FILTER_DOCUMENT_TYPE) } private toggleFilterByItem(item: ObjectWithId, filterRuleTypeID: number) { From 670b6d3629886fb0dff2667a84da4c527203b9f0 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sun, 13 Dec 2020 20:53:00 -0800 Subject: [PATCH 0100/1708] Change date filter active check to check circle filled --- .../filter-dropdown-date/filter-dropdown-date.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html index 7b83f6619..c4befd701 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html @@ -1,8 +1,8 @@ <div class="btn-group" ngbDropdown role="group"> <button class="btn btn-outline-primary btn-sm" id="dropdown{{title}}" ngbDropdownToggle> <ng-container *ngIf="dateBefore || dateAfter"> - <svg 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 xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check-circle-fill text-secondary" viewBox="0 0 16 16"> + <path fill-rule="evenodd" d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/> </svg> </ng-container> {{title}} From 32201dd0349829b7603c4b8fce7ceb146988889a Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sun, 13 Dec 2020 20:56:49 -0800 Subject: [PATCH 0101/1708] button badge margin --- .../filter-dropdown-button.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.html index eef4e2c17..8dff12a33 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.html @@ -8,5 +8,5 @@ <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">{{item.document_count}}</div> + <div class="badge badge-light rounded-pill ml-auto mr-1">{{item.document_count}}</div> </button> From 98ab79ad5ae7d55aa87b15c8212171fdf8c343af Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Mon, 14 Dec 2020 11:12:36 +0100 Subject: [PATCH 0102/1708] fix title filter not removing filter rule --- .../filter-editor.component.html | 2 +- .../filter-editor/filter-editor.component.ts | 41 +++++++++++-------- .../services/filter-editor-view.service.ts | 4 +- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.html b/src-ui/src/app/components/filter-editor/filter-editor.component.html index eb322414d..7b11c4d42 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.html +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.html @@ -3,7 +3,7 @@ <div class="text-muted mt-1">Filter by:</div> </div> <div class="col"> - <input class="form-control form-control-sm" type="text" [(ngModel)]="filterEditorService.filterText" placeholder="Title" #filterTextInput> + <input class="form-control form-control-sm" type="text" [(ngModel)]="filterText" placeholder="Title"> </div> <app-filter-dropdown class="col-auto" [(items)]="filterEditorService.tags" [itemsSelected]="filterEditorService.selectedTags" [title]="'Tags'" (toggle)="onToggleTag($event)"></app-filter-dropdown> diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/filter-editor/filter-editor.component.ts index a6940795e..320322b53 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.ts @@ -1,14 +1,10 @@ -import { Component, EventEmitter, Input, Output, ElementRef, AfterViewInit, QueryList, ViewChild, ViewChildren } from '@angular/core'; -import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'; -import { ObjectWithId } from 'src/app/data/object-with-id'; +import { Component, EventEmitter, Input, Output, OnInit, OnDestroy } from '@angular/core'; import { FilterEditorViewService } from 'src/app/services/filter-editor-view.service' 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 { FilterDropdownComponent } from './filter-dropdown/filter-dropdown.component' -import { FilterDropdownDateComponent } from './filter-dropdown-date/filter-dropdown-date.component' -import { fromEvent } from 'rxjs'; -import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; +import { Subject, Subscription } from 'rxjs'; +import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; @Component({ @@ -16,7 +12,7 @@ import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; templateUrl: './filter-editor.component.html', styleUrls: ['./filter-editor.component.scss'] }) -export class FilterEditorComponent implements AfterViewInit { +export class FilterEditorComponent implements OnInit, OnDestroy { constructor() { } @@ -29,19 +25,32 @@ export class FilterEditorComponent implements AfterViewInit { @Output() apply = new EventEmitter() - @ViewChild('filterTextInput') filterTextInput: ElementRef; + get filterText() { + return this.filterEditorService.filterText + } - ngAfterViewInit() { - fromEvent(this.filterTextInput.nativeElement,'keyup').pipe( - debounceTime(150), - distinctUntilChanged(), - tap() - ).subscribe((event: Event) => { - this.filterEditorService.filterText = (event.target as HTMLInputElement).value + set filterText(value) { + this.filterTextDebounce.next(value) + } + + filterTextDebounce: Subject<string> + subscription: Subscription + + ngOnInit() { + this.filterTextDebounce = new Subject<string>() + this.subscription = this.filterTextDebounce.pipe( + debounceTime(400), + distinctUntilChanged() + ).subscribe(title => { + this.filterEditorService.filterText = title this.applyFilters() }) } + ngOnDestroy() { + this.subscription.unsubscribe() + } + applyFilters() { this.apply.next() } diff --git a/src-ui/src/app/services/filter-editor-view.service.ts b/src-ui/src/app/services/filter-editor-view.service.ts index 89f40189c..27d089106 100644 --- a/src-ui/src/app/services/filter-editor-view.service.ts +++ b/src-ui/src/app/services/filter-editor-view.service.ts @@ -46,7 +46,9 @@ export class FilterEditorViewService { set filterText(text: string) { let filterRules = this.filterRules let existingRule = filterRules.find(rule => rule.type.id == FILTER_TITLE) - if (existingRule && existingRule.value == text) { + if (existingRule && (!text || text.length == 0)) { + filterRules.splice(filterRules.findIndex(rule => rule.type.id == FILTER_TITLE), 1) + } else if (existingRule && existingRule.value == text) { return } else if (existingRule) { existingRule.value = text From 02c1d496d609fbcb11e106ed4a0c71478e8eb725 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Mon, 14 Dec 2020 11:22:24 +0100 Subject: [PATCH 0103/1708] some refactoring. --- .../filter-editor.component.html | 2 +- .../filter-editor/filter-editor.component.ts | 18 +++++----- .../services/filter-editor-view.service.ts | 35 +++++++------------ 3 files changed, 24 insertions(+), 31 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.html b/src-ui/src/app/components/filter-editor/filter-editor.component.html index 7b11c4d42..3452d12b3 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.html +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.html @@ -3,7 +3,7 @@ <div class="text-muted mt-1">Filter by:</div> </div> <div class="col"> - <input class="form-control form-control-sm" type="text" [(ngModel)]="filterText" placeholder="Title"> + <input class="form-control form-control-sm" type="text" [(ngModel)]="titleFilter" placeholder="Title"> </div> <app-filter-dropdown class="col-auto" [(items)]="filterEditorService.tags" [itemsSelected]="filterEditorService.selectedTags" [title]="'Tags'" (toggle)="onToggleTag($event)"></app-filter-dropdown> diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/filter-editor/filter-editor.component.ts index 320322b53..7177d885c 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.ts @@ -25,29 +25,31 @@ export class FilterEditorComponent implements OnInit, OnDestroy { @Output() apply = new EventEmitter() - get filterText() { - return this.filterEditorService.filterText + get titleFilter() { + return this.filterEditorService.titleFilter } - set filterText(value) { - this.filterTextDebounce.next(value) + set titleFilter(value) { + this.titleFilterDebounce.next(value) } - filterTextDebounce: Subject<string> + titleFilterDebounce: Subject<string> subscription: Subscription ngOnInit() { - this.filterTextDebounce = new Subject<string>() - this.subscription = this.filterTextDebounce.pipe( + this.titleFilterDebounce = new Subject<string>() + this.subscription = this.titleFilterDebounce.pipe( debounceTime(400), distinctUntilChanged() ).subscribe(title => { - this.filterEditorService.filterText = title + this.filterEditorService.titleFilter = title this.applyFilters() }) } ngOnDestroy() { + this.titleFilterDebounce.complete() + // TODO: not sure if both is necessary this.subscription.unsubscribe() } diff --git a/src-ui/src/app/services/filter-editor-view.service.ts b/src-ui/src/app/services/filter-editor-view.service.ts index 27d089106..9a6eeef41 100644 --- a/src-ui/src/app/services/filter-editor-view.service.ts +++ b/src-ui/src/app/services/filter-editor-view.service.ts @@ -16,9 +16,6 @@ import { NgbDate, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; providedIn: 'root' }) export class FilterEditorViewService { - private tags$: Observable<Results<PaperlessTag>> - private correspondents$: Observable<Results<PaperlessCorrespondent>> - private documentTypes$: Observable<Results<PaperlessDocumentType>> tags: PaperlessTag[] = [] correspondents: PaperlessCorrespondent[] @@ -27,12 +24,9 @@ export class FilterEditorViewService { filterRules: FilterRule[] = [] constructor(private tagService: TagService, private documentTypeService: DocumentTypeService, private correspondentService: CorrespondentService) { - this.tags$ = this.tagService.listAll() - this.tags$.subscribe(result => this.tags = result.results) - this.correspondents$ = this.correspondentService.listAll() - this.correspondents$.subscribe(result => this.correspondents = result.results) - this.documentTypes$ = this.documentTypeService.listAll() - this.documentTypes$.subscribe(result => this.documentTypes = result.results) + 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) } clear() { @@ -43,22 +37,19 @@ export class FilterEditorViewService { return this.filterRules.length > 0 } - set filterText(text: string) { - let filterRules = this.filterRules - let existingRule = filterRules.find(rule => rule.type.id == FILTER_TITLE) - if (existingRule && (!text || text.length == 0)) { - filterRules.splice(filterRules.findIndex(rule => rule.type.id == FILTER_TITLE), 1) - } else if (existingRule && existingRule.value == text) { - return - } else if (existingRule) { - existingRule.value = text - } else { - filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_TITLE), value: text}) + set titleFilter(title: string) { + let existingRule = this.filterRules.find(rule => rule.type.id == FILTER_TITLE) + + if (!existingRule && title) { + this.filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_TITLE), value: title}) + } else if (existingRule && !title) { + this.filterRules.splice(this.filterRules.findIndex(rule => rule.type.id == FILTER_TITLE), 1) + } else if (existingRule && title) { + existingRule.value = title } - this.filterRules = filterRules } - get filterText(): string { + get titleFilter(): string { let existingRule = this.filterRules.find(rule => rule.type.id == FILTER_TITLE) return existingRule ? existingRule.value : '' } From 10440ec8203d0ac5a44c89d34416114f65891518 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Mon, 14 Dec 2020 11:31:12 +0100 Subject: [PATCH 0104/1708] this button wasn't really doing anything. --- .../filter-dropdown/filter-dropdown.component.html | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html index 0a3fe5496..975e96ec2 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html @@ -12,14 +12,6 @@ <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 class="input-group-append"> - <span class="input-group-text bg-light text-muted"> - <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-search" viewBox="0 0 16 16"> - <path fill-rule="evenodd" d="M10.442 10.442a1 1 0 0 1 1.415 0l3.85 3.85a1 1 0 0 1-1.414 1.415l-3.85-3.85a1 1 0 0 1 0-1.415z"/> - <path fill-rule="evenodd" d="M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0z"/> - </svg> - </span> - </div> </div> </div> <div *ngIf="items" class="items"> From 94c07839a4af0adc8be6e35e42d14818cf33b6ba Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Mon, 14 Dec 2020 16:51:01 +0100 Subject: [PATCH 0105/1708] refactored filter service - I wasn't too happy with that in the end. - The filter editor should not be concerned about managing filter rule state. - Therefore, it should not access a service for filter rules. - The editor should simply be given a set of rules, and edit that rule set. - The only entity that should manage filter state should be the document list service, and the saved view service in the form of filters associated with saved views. --- .../document-list.component.html | 2 +- .../document-list/document-list.component.ts | 41 +--- .../filter-dropdown-date.component.ts | 4 +- .../filter-editor.component.html | 12 +- .../filter-editor/filter-editor.component.ts | 195 +++++++++++++++--- .../filter-editor-view.service.spec.ts | 16 -- .../services/filter-editor-view.service.ts | 182 ---------------- 7 files changed, 186 insertions(+), 266 deletions(-) delete mode 100644 src-ui/src/app/services/filter-editor-view.service.spec.ts delete mode 100644 src-ui/src/app/services/filter-editor-view.service.ts diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index b1b2c8f94..df86507f0 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -63,7 +63,7 @@ </app-page-header> <div class="w-100 mb-4"> - <app-filter-editor [(filterEditorService)]="filterEditorService" (apply)="applyFilterRules()" (clear)="clearFilterRules()" #filterEditor></app-filter-editor> + <app-filter-editor [(filterRules)]="list.filterRules" #filterEditor></app-filter-editor> </div> <div class="d-flex justify-content-between align-items-center"> diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index 8d090f001..f04b5d301 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -2,21 +2,14 @@ import { Component, OnInit, ViewChild } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { cloneFilterRules, FilterRule } from 'src/app/data/filter-rule'; -import { FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; import { SavedViewConfig } from 'src/app/data/saved-view-config'; import { DocumentListViewService } from 'src/app/services/document-list-view.service'; -import { FilterEditorViewService } from 'src/app/services/filter-editor-view.service'; import { DOCUMENT_SORT_FIELDS } from 'src/app/services/rest/document.service'; import { SavedViewConfigService } from 'src/app/services/saved-view-config.service'; import { Toast, ToastService } from 'src/app/services/toast.service'; import { environment } from 'src/environments/environment'; +import { FilterEditorComponent } from '../filter-editor/filter-editor.component'; import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component'; -import { FilterEditorComponent } from 'src/app/components/filter-editor/filter-editor.component'; -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-document-list', templateUrl: './document-list.component.html', @@ -27,26 +20,20 @@ export class DocumentListComponent implements OnInit { constructor( public list: DocumentListViewService, public savedViewConfigService: SavedViewConfigService, - public filterEditorService: FilterEditorViewService, public route: ActivatedRoute, private toastService: ToastService, public modalService: NgbModal, private titleService: Title) { } + @ViewChild("filterEditor") + private filterEditor: FilterEditorComponent + displayMode = 'smallCards' // largeCards, smallCards, details get isFiltered() { return this.list.filterRules?.length > 0 } - set filterRules(filterRules: FilterRule[]) { - this.filterEditorService.filterRules = filterRules - } - - get filterRules(): FilterRule[] { - return this.filterEditorService.filterRules - } - getTitle() { return this.list.savedViewTitle || "Documents" } @@ -66,29 +53,18 @@ export class DocumentListComponent implements OnInit { this.route.paramMap.subscribe(params => { if (params.has('id')) { this.list.savedView = this.savedViewConfigService.getConfig(params.get('id')) - this.filterEditorService.filterRules = this.list.filterRules this.titleService.setTitle(`${this.list.savedView.title} - ${environment.appTitle}`) } else { this.list.savedView = null - this.filterEditorService.filterRules = this.list.filterRules this.titleService.setTitle(`Documents - ${environment.appTitle}`) } this.list.clear() this.list.reload() }) - this.filterEditorService.filterRules = this.list.filterRules } - applyFilterRules() { - this.list.filterRules = this.filterEditorService.filterRules - } - - clearFilterRules() { - this.list.filterRules = this.filterEditorService.filterRules - } loadViewConfig(config: SavedViewConfig) { - this.filterEditorService.filterRules = cloneFilterRules(config.filterRules) this.list.load(config) } @@ -113,18 +89,15 @@ export class DocumentListComponent implements OnInit { } clickTag(tagID: number) { - this.filterEditorService.toggleFilterByTag(tagID) - this.applyFilterRules() + this.filterEditor.toggleTag(tagID) } clickCorrespondent(correspondentID: number) { - this.filterEditorService.toggleFilterByCorrespondent(correspondentID) - this.applyFilterRules() + this.filterEditor.toggleCorrespondent(correspondentID) } clickDocumentType(documentTypeID: number) { - this.filterEditorService.toggleFilterByDocumentType(documentTypeID) - this.applyFilterRules() + this.filterEditor.toggleDocumentType(documentTypeID) } } diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts index f69028bf0..fbe9bdc14 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts @@ -1,6 +1,4 @@ -import { Component, EventEmitter, Input, Output, ElementRef, ViewChild, OnChanges, SimpleChange } from '@angular/core'; -import { FilterRule } from 'src/app/data/filter-rule'; -import { ObjectWithId } from 'src/app/data/object-with-id'; +import { Component, EventEmitter, Input, Output, ElementRef, ViewChild, SimpleChange } from '@angular/core'; import { NgbDate, NgbDateStruct, NgbDatepicker } from '@ng-bootstrap/ng-bootstrap'; @Component({ diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.html b/src-ui/src/app/components/filter-editor/filter-editor.component.html index 3452d12b3..b50ed53e3 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.html +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.html @@ -6,14 +6,14 @@ <input class="form-control form-control-sm" type="text" [(ngModel)]="titleFilter" placeholder="Title"> </div> - <app-filter-dropdown class="col-auto" [(items)]="filterEditorService.tags" [itemsSelected]="filterEditorService.selectedTags" [title]="'Tags'" (toggle)="onToggleTag($event)"></app-filter-dropdown> - <app-filter-dropdown class="col-auto" [(items)]="filterEditorService.correspondents" [itemsSelected]="filterEditorService.selectedCorrespondents" [title]="'Correspondents'" (toggle)="onToggleCorrespondent($event)"></app-filter-dropdown> - <app-filter-dropdown class="col-auto" [(items)]="filterEditorService.documentTypes" [itemsSelected]="filterEditorService.selectedDocumentTypes" [title]="'Document Types'" (toggle)="onToggleDocumentType($event)"></app-filter-dropdown> + <app-filter-dropdown class="col-auto" [items]="tags" [itemsSelected]="selectedTags" title="Tags" (toggle)="toggleTag($event.id)"></app-filter-dropdown> + <app-filter-dropdown class="col-auto" [items]="correspondents" [itemsSelected]="selectedCorrespondents" title="Correspondents" (toggle)="toggleCorrespondent($event.id)"></app-filter-dropdown> + <app-filter-dropdown class="col-auto" [items]="documentTypes" [itemsSelected]="selectedDocumentTypes" title="Document Types" (toggle)="toggleDocumentType($event.id)"></app-filter-dropdown> - <app-filter-dropdown-date class="col-auto" [dateBefore]="filterEditorService.dateCreatedBefore" [dateAfter]="filterEditorService.dateCreatedAfter" [title]="'Created'" (dateBeforeSet)="onDateCreatedBeforeSet($event)" (dateAfterSet)="onDateCreatedAfterSet($event)"></app-filter-dropdown-date> - <app-filter-dropdown-date class="col-auto" [dateBefore]="filterEditorService.dateAddedBefore" [dateAfter]="filterEditorService.dateAddedAfter" [title]="'Added'" (dateBeforeSet)="onDateAddedBeforeSet($event)" (dateAfterSet)="onDateAddedAfterSet($event)"></app-filter-dropdown-date> + <app-filter-dropdown-date class="col-auto" [dateBefore]="dateCreatedBefore" [dateAfter]="dateCreatedAfter" title="Created" (dateBeforeSet)="onDateCreatedBeforeSet($event)" (dateAfterSet)="onDateCreatedAfterSet($event)"></app-filter-dropdown-date> + <app-filter-dropdown-date class="col-auto" [dateBefore]="dateAddedBefore" [dateAfter]="dateAddedAfter" title="Added" (dateBeforeSet)="onDateAddedBeforeSet($event)" (dateAfterSet)="onDateAddedAfterSet($event)"></app-filter-dropdown-date> - <button class="btn btn-link btn-sm" [disabled]="!filterEditorService.hasFilters()" (click)="clearSelected()"> + <button class="btn btn-link btn-sm" [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> diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/filter-editor/filter-editor.component.ts index 7177d885c..a86007e19 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.ts @@ -1,11 +1,15 @@ import { Component, EventEmitter, Input, Output, OnInit, OnDestroy } from '@angular/core'; -import { FilterEditorViewService } from 'src/app/services/filter-editor-view.service' 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 { 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'; @Component({ selector: 'app-filter-editor', @@ -14,19 +18,44 @@ import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; }) export class FilterEditorComponent implements OnInit, OnDestroy { - constructor() { } + constructor( + private documentTypeService: DocumentTypeService, + private tagService: TagService, + private correspondentService: CorrespondentService + ) { } + + tags: PaperlessTag[] = [] + correspondents: PaperlessCorrespondent[] + documentTypes: PaperlessDocumentType[] = [] @Input() - filterEditorService: FilterEditorViewService + filterRules: FilterRule[] @Output() - clear = new EventEmitter() + filterRulesChange = new EventEmitter<FilterRule[]>() + + hasFilters() { + return this.filterRules.length > 0 + } - @Output() - apply = new EventEmitter() + get selectedTags(): PaperlessTag[] { + let tagRules: FilterRule[] = this.filterRules.filter(fr => fr.type.id == 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.type.id == 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.type.id == FILTER_DOCUMENT_TYPE) + return this.documentTypes?.filter(dt => documentTypeRules.find(dtr => dtr.value == dt.id)) + } get titleFilter() { - return this.filterEditorService.titleFilter + let existingRule = this.filterRules.find(rule => rule.type.id == FILTER_TITLE) + return existingRule ? existingRule.value : '' } set titleFilter(value) { @@ -37,13 +66,18 @@ export class FilterEditorComponent implements OnInit, OnDestroy { 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.filterEditorService.titleFilter = title - this.applyFilters() + + this.setTitleRule(title) }) } @@ -54,46 +88,159 @@ export class FilterEditorComponent implements OnInit, OnDestroy { } applyFilters() { - this.apply.next() + this.filterRulesChange.next(this.filterRules) } clearSelected() { - this.filterEditorService.clear() - this.clear.next() - } - - onToggleTag(tag: PaperlessTag) { - this.filterEditorService.toggleFilterByTag(tag) + this.filterRules = [] this.applyFilters() } - onToggleCorrespondent(correspondent: PaperlessCorrespondent) { - this.filterEditorService.toggleFilterByCorrespondent(correspondent) + private toggleFilterRule(filterRuleTypeID: number, value: number) { + + let filterRuleType = FILTER_RULE_TYPES.find(t => t.id == filterRuleTypeID) + + let existingRule = this.filterRules.find(rule => rule.type.id == filterRuleTypeID && rule.value == value) + let existingRuleOfSameType = this.filterRules.find(rule => rule.type.id == 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({type: filterRuleType, value: value}) + } else { + // otherwise (i.e., no multi support AND there's already a rule of this type), update the rule. + existingRuleOfSameType.value = value + } this.applyFilters() } - onToggleDocumentType(documentType: PaperlessDocumentType) { - this.filterEditorService.toggleFilterByDocumentType(documentType) + private setTitleRule(title: string) { + let existingRule = this.filterRules.find(rule => rule.type.id == FILTER_TITLE) + + if (!existingRule && title) { + this.filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_TITLE), value: title}) + } else if (existingRule && !title) { + this.filterRules.splice(this.filterRules.findIndex(rule => rule.type.id == 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 + + onDateCreatedBeforeSet(date: NgbDateStruct) { - this.filterEditorService.setDateCreatedBefore(date) + this.setDateCreatedBefore(date) this.applyFilters() } onDateCreatedAfterSet(date: NgbDateStruct) { - this.filterEditorService.setDateCreatedAfter(date) + this.setDateCreatedAfter(date) this.applyFilters() } onDateAddedBeforeSet(date: NgbDateStruct) { - this.filterEditorService.setDateAddedBefore(date) + this.setDateAddedBefore(date) this.applyFilters() } onDateAddedAfterSet(date: NgbDateStruct) { - this.filterEditorService.setDateAddedAfter(date) + this.setDateAddedAfter(date) this.applyFilters() } + + get dateCreatedBefore(): NgbDateStruct { + let createdBeforeRule: FilterRule = this.filterRules.find(fr => fr.type.id == FILTER_CREATED_BEFORE) + return createdBeforeRule ? { + year: createdBeforeRule.value.substring(0,4), + month: createdBeforeRule.value.substring(5,7), + day: createdBeforeRule.value.substring(8,10) + } : undefined + } + + get dateCreatedAfter(): NgbDateStruct { + let createdAfterRule: FilterRule = this.filterRules.find(fr => fr.type.id == FILTER_CREATED_AFTER) + return createdAfterRule ? { + year: createdAfterRule.value.substring(0,4), + month: createdAfterRule.value.substring(5,7), + day: createdAfterRule.value.substring(8,10) + } : undefined + } + + get dateAddedBefore(): NgbDateStruct { + let addedBeforeRule: FilterRule = this.filterRules.find(fr => fr.type.id == FILTER_ADDED_BEFORE) + return addedBeforeRule ? { + year: addedBeforeRule.value.substring(0,4), + month: addedBeforeRule.value.substring(5,7), + day: addedBeforeRule.value.substring(8,10) + } : undefined + } + + get dateAddedAfter(): NgbDateStruct { + let addedAfterRule: FilterRule = this.filterRules.find(fr => fr.type.id == FILTER_ADDED_AFTER) + return addedAfterRule ? { + year: addedAfterRule.value.substring(0,4), + month: addedAfterRule.value.substring(5,7), + day: addedAfterRule.value.substring(8,10) + } : undefined + } + + setDateCreatedBefore(date?: NgbDateStruct) { + if (date) this.setDateFilter(date, FILTER_CREATED_BEFORE) + else this.clearDateFilter(FILTER_CREATED_BEFORE) + } + + setDateCreatedAfter(date?: NgbDateStruct) { + if (date) this.setDateFilter(date, FILTER_CREATED_AFTER) + else this.clearDateFilter(FILTER_CREATED_AFTER) + } + + setDateAddedBefore(date?: NgbDateStruct) { + if (date) this.setDateFilter(date, FILTER_ADDED_BEFORE) + else this.clearDateFilter(FILTER_ADDED_BEFORE) + } + + setDateAddedAfter(date?: NgbDateStruct) { + if (date) this.setDateFilter(date, FILTER_ADDED_AFTER) + else this.clearDateFilter(FILTER_ADDED_AFTER) + } + + setDateFilter(date: NgbDateStruct, dateRuleTypeID: number) { + let filterRules = this.filterRules + let existingRule = filterRules.find(rule => rule.type.id == dateRuleTypeID) + let newValue = `${date.year}-${date.month.toString().padStart(2,'0')}-${date.day.toString().padStart(2,'0')}` // YYYY-MM-DD + + if (existingRule) { + existingRule.value = newValue + } else { + filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == dateRuleTypeID), value: newValue}) + } + + this.filterRules = filterRules + } + + clearDateFilter(dateRuleTypeID: number) { + let filterRules = this.filterRules + let existingRule = filterRules.find(rule => rule.type.id == dateRuleTypeID) + filterRules.splice(filterRules.indexOf(existingRule), 1) + this.filterRules = filterRules + } + } diff --git a/src-ui/src/app/services/filter-editor-view.service.spec.ts b/src-ui/src/app/services/filter-editor-view.service.spec.ts deleted file mode 100644 index 8051bcf0d..000000000 --- a/src-ui/src/app/services/filter-editor-view.service.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { FilterEditorViewService } from './filter-editor-view.service'; - -describe('FilterEditorViewService', () => { - let service: FilterEditorViewService; - - beforeEach(() => { - TestBed.configureTestingModule({}); - service = TestBed.inject(FilterEditorViewService); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); -}); diff --git a/src-ui/src/app/services/filter-editor-view.service.ts b/src-ui/src/app/services/filter-editor-view.service.ts deleted file mode 100644 index 9a6eeef41..000000000 --- a/src-ui/src/app/services/filter-editor-view.service.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; -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 { ObjectWithId } from 'src/app/data/object-with-id'; -import { FilterRule } from 'src/app/data/filter-rule'; -import { FilterRuleType, FILTER_RULE_TYPES, FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_TITLE, FILTER_ADDED_BEFORE, FILTER_ADDED_AFTER, FILTER_CREATED_BEFORE, FILTER_CREATED_AFTER, FILTER_CREATED_YEAR, FILTER_CREATED_MONTH, FILTER_CREATED_DAY } from 'src/app/data/filter-rule-type'; -import { Results } from 'src/app/data/results' -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 { NgbDate, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; - -@Injectable({ - providedIn: 'root' -}) -export class FilterEditorViewService { - - tags: PaperlessTag[] = [] - correspondents: PaperlessCorrespondent[] - documentTypes: PaperlessDocumentType[] = [] - - filterRules: FilterRule[] = [] - - constructor(private tagService: TagService, private documentTypeService: DocumentTypeService, private correspondentService: CorrespondentService) { - 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) - } - - clear() { - this.filterRules = [] - } - - hasFilters() { - return this.filterRules.length > 0 - } - - set titleFilter(title: string) { - let existingRule = this.filterRules.find(rule => rule.type.id == FILTER_TITLE) - - if (!existingRule && title) { - this.filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_TITLE), value: title}) - } else if (existingRule && !title) { - this.filterRules.splice(this.filterRules.findIndex(rule => rule.type.id == FILTER_TITLE), 1) - } else if (existingRule && title) { - existingRule.value = title - } - } - - get titleFilter(): string { - let existingRule = this.filterRules.find(rule => rule.type.id == FILTER_TITLE) - return existingRule ? existingRule.value : '' - } - - get selectedTags(): PaperlessTag[] { - let tagRules: FilterRule[] = this.filterRules.filter(fr => fr.type.id == 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.type.id == 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.type.id == FILTER_DOCUMENT_TYPE) - return this.documentTypes?.filter(dt => documentTypeRules.find(dtr => dtr.value == dt.id)) - } - - toggleFilterByTag(tag: PaperlessTag | number) { - if (typeof tag == 'number') tag = this.tags?.find(t => t.id == tag) - this.toggleFilterByItem(tag, FILTER_HAS_TAG) - } - - toggleFilterByCorrespondent(correspondent: PaperlessCorrespondent | number) { - if (typeof correspondent == 'number') correspondent = this.correspondents?.find(t => t.id == correspondent) - this.toggleFilterByItem(correspondent, FILTER_CORRESPONDENT) - } - - toggleFilterByDocumentType(documentType: PaperlessDocumentType | number) { - if (typeof documentType == 'number') documentType = this.documentTypes?.find(t => t.id == documentType) - this.toggleFilterByItem(documentType, FILTER_DOCUMENT_TYPE) - } - - private toggleFilterByItem(item: ObjectWithId, filterRuleTypeID: number) { - let filterRules = this.filterRules - let filterRuleType: FilterRuleType = FILTER_RULE_TYPES.find(t => t.id == filterRuleTypeID) - let existingRules = filterRules.filter(rule => rule.type.id == filterRuleType.id) - let existingItemRule = existingRules?.find(rule => rule.value == item.id) - - if (existingRules && existingItemRule) { // if exact rule exists just remove - filterRules.splice(filterRules.indexOf(existingItemRule), 1) - } else if (existingRules.length > 0 && filterRuleType.multi) { // e.g. tags can have multiple - filterRules.push({type: filterRuleType, value: item.id}) - } else if (existingRules.length > 0) { // correspondents & documentTypes can only be one - filterRules.find(rule => rule.type.id == filterRuleType.id).value = item.id - } else { - filterRules.push({type: filterRuleType, value: item.id}) - } - - this.filterRules = filterRules - } - - get dateCreatedBefore(): NgbDateStruct { - let createdBeforeRule: FilterRule = this.filterRules.find(fr => fr.type.id == FILTER_CREATED_BEFORE) - return createdBeforeRule ? { - year: createdBeforeRule.value.substring(0,4), - month: createdBeforeRule.value.substring(5,7), - day: createdBeforeRule.value.substring(8,10) - } : undefined - } - - get dateCreatedAfter(): NgbDateStruct { - let createdAfterRule: FilterRule = this.filterRules.find(fr => fr.type.id == FILTER_CREATED_AFTER) - return createdAfterRule ? { - year: createdAfterRule.value.substring(0,4), - month: createdAfterRule.value.substring(5,7), - day: createdAfterRule.value.substring(8,10) - } : undefined - } - - get dateAddedBefore(): NgbDateStruct { - let addedBeforeRule: FilterRule = this.filterRules.find(fr => fr.type.id == FILTER_ADDED_BEFORE) - return addedBeforeRule ? { - year: addedBeforeRule.value.substring(0,4), - month: addedBeforeRule.value.substring(5,7), - day: addedBeforeRule.value.substring(8,10) - } : undefined - } - - get dateAddedAfter(): NgbDateStruct { - let addedAfterRule: FilterRule = this.filterRules.find(fr => fr.type.id == FILTER_ADDED_AFTER) - return addedAfterRule ? { - year: addedAfterRule.value.substring(0,4), - month: addedAfterRule.value.substring(5,7), - day: addedAfterRule.value.substring(8,10) - } : undefined - } - - setDateCreatedBefore(date?: NgbDateStruct) { - if (date) this.setDateFilter(date, FILTER_CREATED_BEFORE) - else this.clearDateFilter(FILTER_CREATED_BEFORE) - } - - setDateCreatedAfter(date?: NgbDateStruct) { - if (date) this.setDateFilter(date, FILTER_CREATED_AFTER) - else this.clearDateFilter(FILTER_CREATED_AFTER) - } - - setDateAddedBefore(date?: NgbDateStruct) { - if (date) this.setDateFilter(date, FILTER_ADDED_BEFORE) - else this.clearDateFilter(FILTER_ADDED_BEFORE) - } - - setDateAddedAfter(date?: NgbDateStruct) { - if (date) this.setDateFilter(date, FILTER_ADDED_AFTER) - else this.clearDateFilter(FILTER_ADDED_AFTER) - } - - setDateFilter(date: NgbDateStruct, dateRuleTypeID: number) { - let filterRules = this.filterRules - let existingRule = filterRules.find(rule => rule.type.id == dateRuleTypeID) - let newValue = `${date.year}-${date.month.toString().padStart(2,'0')}-${date.day.toString().padStart(2,'0')}` // YYYY-MM-DD - - if (existingRule) { - existingRule.value = newValue - } else { - filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == dateRuleTypeID), value: newValue}) - } - - this.filterRules = filterRules - } - - clearDateFilter(dateRuleTypeID: number) { - let filterRules = this.filterRules - let existingRule = filterRules.find(rule => rule.type.id == dateRuleTypeID) - filterRules.splice(filterRules.indexOf(existingRule), 1) - this.filterRules = filterRules - } -} From 13d934dc6e39bcc617baa032695dc843f4bd611c Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Mon, 14 Dec 2020 18:46:11 +0100 Subject: [PATCH 0106/1708] new saved view service replaces old local storage based service --- src-ui/src/app/data/filter-rule.ts | 6 +- src-ui/src/app/data/paperless-saved-view.ts | 18 +++++ src-ui/src/app/data/saved-view-config.ts | 19 ------ .../services/rest/saved-view.service.spec.ts | 16 +++++ .../app/services/rest/saved-view.service.ts | 53 +++++++++++++++ .../saved-view-config.service.spec.ts | 16 ----- .../app/services/saved-view-config.service.ts | 66 ------------------- 7 files changed, 89 insertions(+), 105 deletions(-) create mode 100644 src-ui/src/app/data/paperless-saved-view.ts delete mode 100644 src-ui/src/app/data/saved-view-config.ts create mode 100644 src-ui/src/app/services/rest/saved-view.service.spec.ts create mode 100644 src-ui/src/app/services/rest/saved-view.service.ts delete mode 100644 src-ui/src/app/services/saved-view-config.service.spec.ts delete mode 100644 src-ui/src/app/services/saved-view-config.service.ts diff --git a/src-ui/src/app/data/filter-rule.ts b/src-ui/src/app/data/filter-rule.ts index 2dc632d9c..a0c6f0086 100644 --- a/src-ui/src/app/data/filter-rule.ts +++ b/src-ui/src/app/data/filter-rule.ts @@ -1,10 +1,8 @@ -import { FilterRuleType } from './filter-rule-type'; - export function cloneFilterRules(filterRules: FilterRule[]): FilterRule[] { if (filterRules) { let newRules: FilterRule[] = [] for (let rule of filterRules) { - newRules.push({type: rule.type, value: rule.value}) + newRules.push({rule_type: rule.rule_type, value: rule.value}) } return newRules } else { @@ -13,6 +11,6 @@ export function cloneFilterRules(filterRules: FilterRule[]): FilterRule[] { } export interface FilterRule { - type: FilterRuleType + rule_type: number value: any } \ No newline at end of file diff --git a/src-ui/src/app/data/paperless-saved-view.ts b/src-ui/src/app/data/paperless-saved-view.ts new file mode 100644 index 000000000..fbc2f5d5e --- /dev/null +++ b/src-ui/src/app/data/paperless-saved-view.ts @@ -0,0 +1,18 @@ +import { FilterRule } from './filter-rule'; +import { ObjectWithId } from './object-with-id'; + +export interface PaperlessSavedView extends ObjectWithId { + + name?: string + + show_on_dashboard?: boolean + + show_in_sidebar?: boolean + + sort_field: string + + sort_reverse: boolean + + filter_rules: FilterRule[] + +} \ No newline at end of file diff --git a/src-ui/src/app/data/saved-view-config.ts b/src-ui/src/app/data/saved-view-config.ts deleted file mode 100644 index 9d7076215..000000000 --- a/src-ui/src/app/data/saved-view-config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { FilterRule } from './filter-rule'; - -export interface SavedViewConfig { - - id?: string - - filterRules: FilterRule[] - - sortField: string - - sortDirection: string - - title?: string - - showInSideBar?: boolean - - showInDashboard?: boolean - -} \ No newline at end of file diff --git a/src-ui/src/app/services/rest/saved-view.service.spec.ts b/src-ui/src/app/services/rest/saved-view.service.spec.ts new file mode 100644 index 000000000..588cf6347 --- /dev/null +++ b/src-ui/src/app/services/rest/saved-view.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { SavedViewService } from './saved-view.service'; + +describe('SavedViewService', () => { + let service: SavedViewService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SavedViewService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src-ui/src/app/services/rest/saved-view.service.ts b/src-ui/src/app/services/rest/saved-view.service.ts new file mode 100644 index 000000000..343b1a8f8 --- /dev/null +++ b/src-ui/src/app/services/rest/saved-view.service.ts @@ -0,0 +1,53 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Subject } from 'rxjs'; +import { map, tap } from 'rxjs/operators'; +import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'; +import { AbstractPaperlessService } from './abstract-paperless-service'; + +@Injectable({ + providedIn: 'root' +}) +export class SavedViewService extends AbstractPaperlessService<PaperlessSavedView> { + + constructor(http: HttpClient) { + super(http, 'saved_views') + this.reload() + } + + private reload() { + this.listAll().subscribe(r => this.savedViews = r.results) + } + + private savedViews: PaperlessSavedView[] = [] + + get allViews() { + return this.savedViews + } + + get sidebarViews() { + return this.savedViews.filter(v => v.show_in_sidebar) + } + + get dashboardViews() { + return this.savedViews.filter(v => v.show_on_dashboard) + } + + create(o: PaperlessSavedView) { + return super.create(o).pipe( + tap(() => this.reload()) + ) + } + + update(o: PaperlessSavedView) { + return super.update(o).pipe( + tap(() => this.reload()) + ) + } + + delete(o: PaperlessSavedView) { + return super.delete(o).pipe( + tap(() => this.reload()) + ) + } +} diff --git a/src-ui/src/app/services/saved-view-config.service.spec.ts b/src-ui/src/app/services/saved-view-config.service.spec.ts deleted file mode 100644 index c67affead..000000000 --- a/src-ui/src/app/services/saved-view-config.service.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { SavedViewConfigService } from './saved-view-config.service'; - -describe('SavedViewConfigService', () => { - let service: SavedViewConfigService; - - beforeEach(() => { - TestBed.configureTestingModule({}); - service = TestBed.inject(SavedViewConfigService); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); -}); diff --git a/src-ui/src/app/services/saved-view-config.service.ts b/src-ui/src/app/services/saved-view-config.service.ts deleted file mode 100644 index 41c28216b..000000000 --- a/src-ui/src/app/services/saved-view-config.service.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Injectable } from '@angular/core'; -import { v4 as uuidv4 } from 'uuid'; -import { SavedViewConfig } from '../data/saved-view-config'; - -@Injectable({ - providedIn: 'root' -}) -export class SavedViewConfigService { - - constructor() { - let savedConfigs = localStorage.getItem('saved-view-config-service:savedConfigs') - if (savedConfigs) { - try { - this.configs = JSON.parse(savedConfigs) - } catch (e) { - this.configs = [] - } - } - } - - private configs: SavedViewConfig[] = [] - - getConfigs(): SavedViewConfig[] { - return this.configs - } - - getDashboardConfigs(): SavedViewConfig[] { - return this.configs.filter(sf => sf.showInDashboard) - } - - getSideBarConfigs(): SavedViewConfig[] { - return this.configs.filter(sf => sf.showInSideBar) - } - - getConfig(id: string): SavedViewConfig { - return this.configs.find(sf => sf.id == id) - } - - newConfig(config: SavedViewConfig) { - config.id = uuidv4() - this.configs.push(config) - - this.save() - } - - updateConfig(config: SavedViewConfig) { - let savedConfig = this.configs.find(c => c.id == config.id) - if (savedConfig) { - Object.assign(savedConfig, config) - this.save() - } - } - - private save() { - localStorage.setItem('saved-view-config-service:savedConfigs', JSON.stringify(this.configs)) - } - - deleteConfig(config: SavedViewConfig) { - let index = this.configs.findIndex(vc => vc.id == config.id) - if (index != -1) { - this.configs.splice(index, 1) - this.save() - } - - } -} From b7126030d197db4cae0f8f2ac46226bf4effcfe4 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Mon, 14 Dec 2020 19:26:36 +0100 Subject: [PATCH 0107/1708] many changes to support server side saved views --- .../app-frame/app-frame.component.html | 8 +-- .../app-frame/app-frame.component.ts | 4 +- .../dashboard/dashboard.component.ts | 11 ++-- .../saved-view-widget.component.html | 2 +- .../saved-view-widget.component.ts | 8 +-- .../document-list.component.html | 10 ++-- .../document-list/document-list.component.ts | 53 ++++++++++++------- .../save-view-config-dialog.component.html | 4 +- .../save-view-config-dialog.component.ts | 4 +- .../filter-editor/filter-editor.component.ts | 34 ++++++------ .../generic-list/generic-list.component.ts | 7 +-- .../components/manage/logs/logs.component.ts | 4 +- .../manage/settings/settings.component.html | 10 ++-- .../manage/settings/settings.component.ts | 11 ++-- .../services/document-list-view.service.ts | 49 ++++++++--------- .../rest/abstract-paperless-service.ts | 12 ++--- .../src/app/services/rest/document.service.ts | 17 +++--- 17 files changed, 131 insertions(+), 117 deletions(-) diff --git a/src-ui/src/app/components/app-frame/app-frame.component.html b/src-ui/src/app/components/app-frame/app-frame.component.html index 1cedeefde..7876150af 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.html +++ b/src-ui/src/app/components/app-frame/app-frame.component.html @@ -37,16 +37,16 @@ </li> </ul> - <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted" *ngIf='viewConfigService.getSideBarConfigs().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> </h6> <ul class="nav flex-column mb-2"> - <li class="nav-item w-100" *ngFor='let config of viewConfigService.getSideBarConfigs()'> - <a class="nav-link text-truncate" routerLink="view/{{config.id}}" routerLinkActive="active" (click)="closeMenu()"> + <li class="nav-item w-100" *ngFor="let view of savedViewService.sidebarViews"> + <a class="nav-link text-truncate" routerLink="view/{{view.id}}" routerLinkActive="active" (click)="closeMenu()"> <svg class="sidebaricon" fill="currentColor"> <use xlink:href="assets/bootstrap-icons.svg#funnel"/> </svg> - {{config.title}} + {{view.name}} </a> </li> </ul> diff --git a/src-ui/src/app/components/app-frame/app-frame.component.ts b/src-ui/src/app/components/app-frame/app-frame.component.ts index 34e804db4..ef859bf35 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.ts +++ b/src-ui/src/app/components/app-frame/app-frame.component.ts @@ -5,8 +5,8 @@ import { from, Observable, Subscription } from 'rxjs'; import { debounceTime, distinctUntilChanged, map, switchMap } from 'rxjs/operators'; import { PaperlessDocument } from 'src/app/data/paperless-document'; import { OpenDocumentsService } from 'src/app/services/open-documents.service'; +import { SavedViewService } from 'src/app/services/rest/saved-view.service'; import { SearchService } from 'src/app/services/rest/search.service'; -import { SavedViewConfigService } from 'src/app/services/saved-view-config.service'; import { DocumentDetailComponent } from '../document-detail/document-detail.component'; @Component({ @@ -21,7 +21,7 @@ export class AppFrameComponent implements OnInit, OnDestroy { private activatedRoute: ActivatedRoute, private openDocumentsService: OpenDocumentsService, private searchService: SearchService, - public viewConfigService: SavedViewConfigService + public savedViewService: SavedViewService ) { } diff --git a/src-ui/src/app/components/dashboard/dashboard.component.ts b/src-ui/src/app/components/dashboard/dashboard.component.ts index c7410c3f2..57744d194 100644 --- a/src-ui/src/app/components/dashboard/dashboard.component.ts +++ b/src-ui/src/app/components/dashboard/dashboard.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { Title } from '@angular/platform-browser'; -import { SavedViewConfigService } from 'src/app/services/saved-view-config.service'; +import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'; +import { SavedViewService } from 'src/app/services/rest/saved-view.service'; import { environment } from 'src/environments/environment'; @@ -12,14 +13,16 @@ import { environment } from 'src/environments/environment'; export class DashboardComponent implements OnInit { constructor( - public savedViewConfigService: SavedViewConfigService, + private savedViewService: SavedViewService, private titleService: Title) { } - savedViews = [] + savedViews: PaperlessSavedView[] = [] ngOnInit(): void { - this.savedViews = this.savedViewConfigService.getDashboardConfigs() + this.savedViewService.listAll().subscribe(results => { + this.savedViews = results.results.filter(savedView => savedView.show_on_dashboard) + }) this.titleService.setTitle(`Dashboard - ${environment.appTitle}`) } diff --git a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html index e63ecc47b..194497d39 100644 --- a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html +++ b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html @@ -1,4 +1,4 @@ -<app-widget-frame [title]="savedView.title"> +<app-widget-frame [title]="savedView.name"> <a header-buttons [routerLink]="" (click)="showAll()">Show all</a> diff --git a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts index a55bf57fc..5bfecc640 100644 --- a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts +++ b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts @@ -1,7 +1,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { PaperlessDocument } from 'src/app/data/paperless-document'; -import { SavedViewConfig } from 'src/app/data/saved-view-config'; +import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'; import { DocumentListViewService } from 'src/app/services/document-list-view.service'; import { DocumentService } from 'src/app/services/rest/document.service'; @@ -18,18 +18,18 @@ export class SavedViewWidgetComponent implements OnInit { private list: DocumentListViewService) { } @Input() - savedView: SavedViewConfig + savedView: PaperlessSavedView documents: PaperlessDocument[] = [] ngOnInit(): void { - this.documentService.list(1,10,this.savedView.sortField,this.savedView.sortDirection,this.savedView.filterRules).subscribe(result => { + this.documentService.list(1,10,this.savedView.sort_field, this.savedView.sort_reverse, this.savedView.filter_rules).subscribe(result => { this.documents = result.results }) } showAll() { - if (this.savedView.showInSideBar) { + if (this.savedView.show_in_sidebar) { this.router.navigate(['view', this.savedView.id]) } else { this.list.load(this.savedView) diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index df86507f0..acbfd3602 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -21,7 +21,7 @@ </label> </div> - <div class="btn-group btn-group-toggle ml-2" ngbRadioGroup [(ngModel)]="list.sortDirection"> + <div class="btn-group btn-group-toggle ml-2" ngbRadioGroup [(ngModel)]="list.sortReverse"> <div ngbDropdown class="btn-group"> <button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle>Sort by</button> <div ngbDropdownMenu aria-labelledby="dropdownBasic1" class="shadow"> @@ -30,13 +30,13 @@ </div> </div> <label ngbButtonLabel class="btn-outline-primary btn-sm"> - <input ngbButton type="radio" class="btn btn-sm" value="asc"> + <input ngbButton type="radio" class="btn btn-sm" [value]="false"> <svg class="toolbaricon" fill="currentColor"> <use xlink:href="assets/bootstrap-icons.svg#sort-alpha-down" /> </svg> </label> <label ngbButtonLabel class="btn-outline-primary btn-sm"> - <input ngbButton type="radio" class="btn btn-sm" value="des"> + <input ngbButton type="radio" class="btn btn-sm" [value]="true"> <svg class="toolbaricon" fill="currentColor"> <use xlink:href="assets/bootstrap-icons.svg#sort-alpha-up-alt" /> </svg> @@ -49,8 +49,8 @@ <button class="btn btn-sm btn-outline-primary dropdown-toggle" ngbDropdownToggle>Views</button> <div class="dropdown-menu shadow" ngbDropdownMenu> <ng-container *ngIf="!list.savedViewId"> - <button ngbDropdownItem *ngFor="let config of savedViewConfigService.getConfigs()" (click)="loadViewConfig(config)">{{config.title}}</button> - <div class="dropdown-divider" *ngIf="savedViewConfigService.getConfigs().length > 0"></div> + <button ngbDropdownItem *ngFor="let view of savedViewService.allViews" (click)="loadViewConfig(view)">{{view.name}}</button> + <div class="dropdown-divider" *ngIf="savedViewService.allViews.length > 0"></div> </ng-container> <button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.savedViewId">Save "{{list.savedViewTitle}}"</button> diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index f04b5d301..eb3b89db1 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -1,15 +1,16 @@ import { Component, OnInit, ViewChild } from '@angular/core'; import { Title } from '@angular/platform-browser'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { SavedViewConfig } from 'src/app/data/saved-view-config'; +import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'; import { DocumentListViewService } from 'src/app/services/document-list-view.service'; import { DOCUMENT_SORT_FIELDS } from 'src/app/services/rest/document.service'; -import { SavedViewConfigService } from 'src/app/services/saved-view-config.service'; +import { SavedViewService } from 'src/app/services/rest/saved-view.service'; import { Toast, ToastService } from 'src/app/services/toast.service'; import { environment } from 'src/environments/environment'; import { FilterEditorComponent } from '../filter-editor/filter-editor.component'; import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component'; + @Component({ selector: 'app-document-list', templateUrl: './document-list.component.html', @@ -19,8 +20,9 @@ export class DocumentListComponent implements OnInit { constructor( public list: DocumentListViewService, - public savedViewConfigService: SavedViewConfigService, + public savedViewService: SavedViewService, public route: ActivatedRoute, + private router: Router, private toastService: ToastService, public modalService: NgbModal, private titleService: Title) { } @@ -51,40 +53,51 @@ export class DocumentListComponent implements OnInit { this.displayMode = localStorage.getItem('document-list:displayMode') } this.route.paramMap.subscribe(params => { + this.list.clear() if (params.has('id')) { - this.list.savedView = this.savedViewConfigService.getConfig(params.get('id')) - this.titleService.setTitle(`${this.list.savedView.title} - ${environment.appTitle}`) + this.savedViewService.getCached(+params.get('id')).subscribe(view => { + if (!view) { + this.router.navigate(["404"]) + return + } + + this.list.savedView = view + this.titleService.setTitle(`${this.list.savedView.name} - ${environment.appTitle}`) + this.list.reload() + }) } else { this.list.savedView = null this.titleService.setTitle(`Documents - ${environment.appTitle}`) + this.list.reload() } - this.list.clear() - this.list.reload() }) } - loadViewConfig(config: SavedViewConfig) { - this.list.load(config) + loadViewConfig(view: PaperlessSavedView) { + this.list.load(view) } saveViewConfig() { - this.savedViewConfigService.updateConfig(this.list.savedView) - this.toastService.showToast(Toast.make("Information", `View "${this.list.savedView.title}" saved successfully.`)) + this.savedViewService.update(this.list.savedView).subscribe(result => { + this.toastService.showToast(Toast.make("Information", `View "${this.list.savedView.name}" saved successfully.`)) + }) + } saveViewConfigAs() { let modal = this.modalService.open(SaveViewConfigDialogComponent, {backdrop: 'static'}) modal.componentInstance.saveClicked.subscribe(formValue => { - this.savedViewConfigService.newConfig({ - title: formValue.title, - showInDashboard: formValue.showInDashboard, - showInSideBar: formValue.showInSideBar, - filterRules: this.list.filterRules, - sortDirection: this.list.sortDirection, - sortField: this.list.sortField + this.savedViewService.create({ + name: formValue.name, + show_on_dashboard: formValue.showOnDashboard, + show_in_sidebar: formValue.showInSideBar, + filter_rules: this.list.filterRules, + sort_reverse: this.list.sortReverse, + sort_field: this.list.sortField + }).subscribe(() => { + modal.close() }) - modal.close() }) } diff --git a/src-ui/src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.html b/src-ui/src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.html index 870431096..8819aa313 100644 --- a/src-ui/src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.html +++ b/src-ui/src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.html @@ -6,9 +6,9 @@ </button> </div> <div class="modal-body"> - <app-input-text title="Title" formControlName="title"></app-input-text> + <app-input-text title="Name" formControlName="name"></app-input-text> <app-input-check title="Show in side bar" formControlName="showInSideBar"></app-input-check> - <app-input-check title="Show in dashboard" formControlName="showInDashboard"></app-input-check> + <app-input-check title="Show on dashboard" formControlName="showOnDashboard"></app-input-check> </div> <div class="modal-footer"> <button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button> diff --git a/src-ui/src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.ts b/src-ui/src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.ts index 0dd351770..284be49f6 100644 --- a/src-ui/src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.ts +++ b/src-ui/src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.ts @@ -15,9 +15,9 @@ export class SaveViewConfigDialogComponent implements OnInit { public saveClicked = new EventEmitter() saveViewConfigForm = new FormGroup({ - title: new FormControl(''), + name: new FormControl(''), showInSideBar: new FormControl(false), - showInDashboard: new FormControl(false), + showOnDashboard: new FormControl(false), }) ngOnInit(): void { diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/filter-editor/filter-editor.component.ts index a86007e19..c08b12bee 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.ts @@ -39,22 +39,22 @@ export class FilterEditorComponent implements OnInit, OnDestroy { } get selectedTags(): PaperlessTag[] { - let tagRules: FilterRule[] = this.filterRules.filter(fr => fr.type.id == FILTER_HAS_TAG) + 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.type.id == FILTER_CORRESPONDENT) + 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.type.id == FILTER_DOCUMENT_TYPE) + 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.type.id == FILTER_TITLE) + let existingRule = this.filterRules.find(rule => rule.rule_type == FILTER_TITLE) return existingRule ? existingRule.value : '' } @@ -100,15 +100,15 @@ export class FilterEditorComponent implements OnInit, OnDestroy { let filterRuleType = FILTER_RULE_TYPES.find(t => t.id == filterRuleTypeID) - let existingRule = this.filterRules.find(rule => rule.type.id == filterRuleTypeID && rule.value == value) - let existingRuleOfSameType = this.filterRules.find(rule => rule.type.id == filterRuleTypeID) + let existingRule = this.filterRules.find(rule => rule.rule_type == filterRuleTypeID && rule.value == value) + 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({type: filterRuleType, value: value}) + this.filterRules.push({rule_type: filterRuleTypeID, value: value}) } else { // otherwise (i.e., no multi support AND there's already a rule of this type), update the rule. existingRuleOfSameType.value = value @@ -117,12 +117,12 @@ export class FilterEditorComponent implements OnInit, OnDestroy { } private setTitleRule(title: string) { - let existingRule = this.filterRules.find(rule => rule.type.id == FILTER_TITLE) + let existingRule = this.filterRules.find(rule => rule.rule_type == FILTER_TITLE) if (!existingRule && title) { - this.filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_TITLE), value: title}) + this.filterRules.push({rule_type: FILTER_TITLE, value: title}) } else if (existingRule && !title) { - this.filterRules.splice(this.filterRules.findIndex(rule => rule.type.id == FILTER_TITLE), 1) + this.filterRules.splice(this.filterRules.findIndex(rule => rule.rule_type == FILTER_TITLE), 1) } else if (existingRule && title) { existingRule.value = title } @@ -167,7 +167,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy { } get dateCreatedBefore(): NgbDateStruct { - let createdBeforeRule: FilterRule = this.filterRules.find(fr => fr.type.id == FILTER_CREATED_BEFORE) + let createdBeforeRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_CREATED_BEFORE) return createdBeforeRule ? { year: createdBeforeRule.value.substring(0,4), month: createdBeforeRule.value.substring(5,7), @@ -176,7 +176,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy { } get dateCreatedAfter(): NgbDateStruct { - let createdAfterRule: FilterRule = this.filterRules.find(fr => fr.type.id == FILTER_CREATED_AFTER) + let createdAfterRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_CREATED_AFTER) return createdAfterRule ? { year: createdAfterRule.value.substring(0,4), month: createdAfterRule.value.substring(5,7), @@ -185,7 +185,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy { } get dateAddedBefore(): NgbDateStruct { - let addedBeforeRule: FilterRule = this.filterRules.find(fr => fr.type.id == FILTER_ADDED_BEFORE) + let addedBeforeRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_ADDED_BEFORE) return addedBeforeRule ? { year: addedBeforeRule.value.substring(0,4), month: addedBeforeRule.value.substring(5,7), @@ -194,7 +194,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy { } get dateAddedAfter(): NgbDateStruct { - let addedAfterRule: FilterRule = this.filterRules.find(fr => fr.type.id == FILTER_ADDED_AFTER) + let addedAfterRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_ADDED_AFTER) return addedAfterRule ? { year: addedAfterRule.value.substring(0,4), month: addedAfterRule.value.substring(5,7), @@ -224,13 +224,13 @@ export class FilterEditorComponent implements OnInit, OnDestroy { setDateFilter(date: NgbDateStruct, dateRuleTypeID: number) { let filterRules = this.filterRules - let existingRule = filterRules.find(rule => rule.type.id == dateRuleTypeID) + let existingRule = filterRules.find(rule => rule.rule_type == dateRuleTypeID) let newValue = `${date.year}-${date.month.toString().padStart(2,'0')}-${date.day.toString().padStart(2,'0')}` // YYYY-MM-DD if (existingRule) { existingRule.value = newValue } else { - filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == dateRuleTypeID), value: newValue}) + filterRules.push({rule_type: dateRuleTypeID, value: newValue}) } this.filterRules = filterRules @@ -238,7 +238,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy { clearDateFilter(dateRuleTypeID: number) { let filterRules = this.filterRules - let existingRule = filterRules.find(rule => rule.type.id == dateRuleTypeID) + let existingRule = filterRules.find(rule => rule.rule_type == dateRuleTypeID) filterRules.splice(filterRules.indexOf(existingRule), 1) this.filterRules = filterRules } diff --git a/src-ui/src/app/components/manage/generic-list/generic-list.component.ts b/src-ui/src/app/components/manage/generic-list/generic-list.component.ts index 59a5f09ed..76a92e4e9 100644 --- a/src-ui/src/app/components/manage/generic-list/generic-list.component.ts +++ b/src-ui/src/app/components/manage/generic-list/generic-list.component.ts @@ -8,9 +8,9 @@ import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dial @Directive() export abstract class GenericListComponent<T extends ObjectWithId> implements OnInit { - + constructor( - private service: AbstractPaperlessService<T>, + private service: AbstractPaperlessService<T>, private modalService: NgbModal, private editDialogComponent: any) { } @@ -60,7 +60,8 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On } reloadData() { - this.service.list(this.page, null, this.sortField, this.sortDirection).subscribe(c => { + // TODO: this is a hack + this.service.list(this.page, null, this.sortField, this.sortDirection == 'des').subscribe(c => { this.data = c.results this.collectionSize = c.count }); diff --git a/src-ui/src/app/components/manage/logs/logs.component.ts b/src-ui/src/app/components/manage/logs/logs.component.ts index 44d0fa24d..131f91f9c 100644 --- a/src-ui/src/app/components/manage/logs/logs.component.ts +++ b/src-ui/src/app/components/manage/logs/logs.component.ts @@ -22,7 +22,7 @@ export class LogsComponent implements OnInit { } reload() { - this.logService.list(1, 50, 'created', 'des', {'level__gte': this.level}).subscribe(result => this.logs = result.results) + this.logService.list(1, 50, 'created', true, {'level__gte': this.level}).subscribe(result => this.logs = result.results) } getLevelText(level: number) { @@ -34,7 +34,7 @@ export class LogsComponent implements OnInit { if (this.logs.length > 0) { lastCreated = new Date(this.logs[this.logs.length-1].created).toISOString() } - this.logService.list(1, 25, 'created', 'des', {'created__lt': lastCreated, 'level__gte': this.level}).subscribe(result => { + this.logService.list(1, 25, 'created', true, {'created__lt': lastCreated, 'level__gte': this.level}).subscribe(result => { this.logs.push(...result.results) }) } diff --git a/src-ui/src/app/components/manage/settings/settings.component.html b/src-ui/src/app/components/manage/settings/settings.component.html index 7a500e6eb..73e4f8194 100644 --- a/src-ui/src/app/components/manage/settings/settings.component.html +++ b/src-ui/src/app/components/manage/settings/settings.component.html @@ -44,11 +44,11 @@ </tr> </thead> <tbody> - <tr *ngFor="let config of savedViewConfigService.getConfigs()"> - <td>{{ config.title }}</td> - <td>{{ config.showInDashboard | yesno }}</td> - <td>{{ config.showInSideBar | yesno }}</td> - <td><button type="button" class="btn btn-sm btn-outline-danger" (click)="deleteViewConfig(config)">Delete</button></td> + <tr *ngFor="let view of savedViewService.allViews"> + <td>{{ view.name }}</td> + <td>{{ view.show_on_dashboard | yesno }}</td> + <td>{{ view.show_in_sidebar | yesno }}</td> + <td><button type="button" class="btn btn-sm btn-outline-danger" (click)="deleteSavedView(view)">Delete</button></td> </tr> </tbody> </table> diff --git a/src-ui/src/app/components/manage/settings/settings.component.ts b/src-ui/src/app/components/manage/settings/settings.component.ts index c7b976c65..8ceee6e03 100644 --- a/src-ui/src/app/components/manage/settings/settings.component.ts +++ b/src-ui/src/app/components/manage/settings/settings.component.ts @@ -1,10 +1,11 @@ import { Component, OnInit } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; import { Title } from '@angular/platform-browser'; -import { SavedViewConfig } from 'src/app/data/saved-view-config'; +import { map, tap } from 'rxjs/operators'; +import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'; import { GENERAL_SETTINGS } from 'src/app/data/storage-keys'; import { DocumentListViewService } from 'src/app/services/document-list-view.service'; -import { SavedViewConfigService } from 'src/app/services/saved-view-config.service'; +import { SavedViewService } from 'src/app/services/rest/saved-view.service'; import { environment } from 'src/environments/environment'; @Component({ @@ -19,7 +20,7 @@ export class SettingsComponent implements OnInit { }) constructor( - private savedViewConfigService: SavedViewConfigService, + public savedViewService: SavedViewService, private documentListViewService: DocumentListViewService, private titleService: Title ) { } @@ -28,8 +29,8 @@ export class SettingsComponent implements OnInit { this.titleService.setTitle(`Settings - ${environment.appTitle}`) } - deleteViewConfig(config: SavedViewConfig) { - this.savedViewConfigService.deleteConfig(config) + deleteSavedView(savedView: PaperlessSavedView) { + this.savedViewService.delete(savedView).subscribe(() => {}) } saveSettings() { diff --git a/src-ui/src/app/services/document-list-view.service.ts b/src-ui/src/app/services/document-list-view.service.ts index 8692ed1c0..4fa5e23d9 100644 --- a/src-ui/src/app/services/document-list-view.service.ts +++ b/src-ui/src/app/services/document-list-view.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { cloneFilterRules, FilterRule } from '../data/filter-rule'; import { PaperlessDocument } from '../data/paperless-document'; -import { SavedViewConfig } from '../data/saved-view-config'; +import { PaperlessSavedView } from '../data/paperless-saved-view'; import { DOCUMENT_LIST_SERVICE, GENERAL_SETTINGS } from '../data/storage-keys'; import { DocumentService } from './rest/document.service'; @@ -29,17 +29,17 @@ export class DocumentListViewService { /** * This is the current config for the document list. The service will always remember the last settings used for the document list. */ - private _documentListViewConfig: SavedViewConfig + private _documentListViewConfig: PaperlessSavedView /** * Optionally, this is the currently selected saved view, which might be null. */ - private _savedViewConfig: SavedViewConfig + private _savedViewConfig: PaperlessSavedView - get savedView() { + get savedView(): PaperlessSavedView { return this._savedViewConfig } - set savedView(value) { + set savedView(value: PaperlessSavedView) { if (value) { //this is here so that we don't modify value, which might be the actual instance of the saved view. this._savedViewConfig = Object.assign({}, value) @@ -53,7 +53,7 @@ export class DocumentListViewService { } get savedViewTitle() { - return this.savedView?.title + return this.savedView?.name } get documentListView() { @@ -75,10 +75,11 @@ export class DocumentListViewService { return this.savedView || this.documentListView } - load(config: SavedViewConfig) { - this.view.filterRules = cloneFilterRules(config.filterRules) - this.view.sortDirection = config.sortDirection - this.view.sortField = config.sortField + load(view: PaperlessSavedView) { + this.view.filter_rules = cloneFilterRules(view.filter_rules) + this.view.sort_reverse = view.sort_reverse + this.view.sort_field = view.sort_field + this.saveDocumentListView() this.reload() } @@ -93,9 +94,9 @@ export class DocumentListViewService { this.documentService.list( this.currentPage, this.currentPageSize, - this.view.sortField, - this.view.sortDirection, - this.view.filterRules).subscribe( + this.view.sort_field, + this.view.sort_reverse, + this.view.filter_rules).subscribe( result => { this.collectionSize = result.count this.documents = result.results @@ -116,33 +117,33 @@ export class DocumentListViewService { set filterRules(filterRules: FilterRule[]) { //we're going to clone the filterRules object, since we don't //want changes in the filter editor to propagate into here right away. - this.view.filterRules = cloneFilterRules(filterRules) + this.view.filter_rules = cloneFilterRules(filterRules) this.reload() this.saveDocumentListView() } get filterRules(): FilterRule[] { - return cloneFilterRules(this.view.filterRules) + return cloneFilterRules(this.view.filter_rules) } set sortField(field: string) { - this.view.sortField = field + this.view.sort_field = field this.saveDocumentListView() this.reload() } get sortField(): string { - return this.view.sortField + return this.view.sort_field } - set sortDirection(direction: string) { - this.view.sortDirection = direction + set sortReverse(reverse: boolean) { + this.view.sort_reverse = reverse this.saveDocumentListView() this.reload() } - get sortDirection(): string { - return this.view.sortDirection + get sortReverse(): boolean { + return this.view.sort_reverse } private saveDocumentListView() { @@ -204,9 +205,9 @@ export class DocumentListViewService { } if (!this.documentListView) { this.documentListView = { - filterRules: [], - sortDirection: 'des', - sortField: 'created' + filter_rules: [], + sort_reverse: true, + sort_field: 'created' } } } diff --git a/src-ui/src/app/services/rest/abstract-paperless-service.ts b/src-ui/src/app/services/rest/abstract-paperless-service.ts index 3feed320e..396baa1c5 100644 --- a/src-ui/src/app/services/rest/abstract-paperless-service.ts +++ b/src-ui/src/app/services/rest/abstract-paperless-service.ts @@ -22,17 +22,15 @@ export abstract class AbstractPaperlessService<T extends ObjectWithId> { return url } - private getOrderingQueryParam(sortField: string, sortDirection: string) { - if (sortField && sortDirection) { - return (sortDirection == 'des' ? '-' : '') + sortField - } else if (sortField) { - return sortField + private getOrderingQueryParam(sortField: string, sortReverse: boolean) { + if (sortField) { + return (sortReverse ? '-' : '') + sortField } else { return null } } - list(page?: number, pageSize?: number, sortField?: string, sortDirection?: string, extraParams?): Observable<Results<T>> { + list(page?: number, pageSize?: number, sortField?: string, sortReverse?: boolean, extraParams?): Observable<Results<T>> { let httpParams = new HttpParams() if (page) { httpParams = httpParams.set('page', page.toString()) @@ -40,7 +38,7 @@ export abstract class AbstractPaperlessService<T extends ObjectWithId> { if (pageSize) { httpParams = httpParams.set('page_size', pageSize.toString()) } - let ordering = this.getOrderingQueryParam(sortField, sortDirection) + let ordering = this.getOrderingQueryParam(sortField, sortReverse) if (ordering) { httpParams = httpParams.set('ordering', ordering) } diff --git a/src-ui/src/app/services/rest/document.service.ts b/src-ui/src/app/services/rest/document.service.ts index 81693ec68..f50620d23 100644 --- a/src-ui/src/app/services/rest/document.service.ts +++ b/src-ui/src/app/services/rest/document.service.ts @@ -10,7 +10,7 @@ import { map } from 'rxjs/operators'; import { CorrespondentService } from './correspondent.service'; import { DocumentTypeService } from './document-type.service'; import { TagService } from './tag.service'; - +import { FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; export const DOCUMENT_SORT_FIELDS = [ { field: "correspondent__name", name: "Correspondent" }, @@ -22,10 +22,6 @@ export const DOCUMENT_SORT_FIELDS = [ { field: 'modified', name: 'Modified' } ] -export const SORT_DIRECTION_ASCENDING = "asc" -export const SORT_DIRECTION_DESCENDING = "des" - - @Injectable({ providedIn: 'root' }) @@ -39,10 +35,11 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument> if (filterRules) { let params = {} for (let rule of filterRules) { - if (rule.type.multi) { - params[rule.type.filtervar] = params[rule.type.filtervar] ? params[rule.type.filtervar] + "," + rule.value : rule.value + let ruleType = FILTER_RULE_TYPES.find(t => t.id == rule.rule_type) + if (ruleType.multi) { + params[ruleType.filtervar] = params[ruleType.filtervar] ? params[ruleType.filtervar] + "," + rule.value : rule.value } else { - params[rule.type.filtervar] = rule.value + params[ruleType.filtervar] = rule.value } } return params @@ -64,8 +61,8 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument> return doc } - list(page?: number, pageSize?: number, sortField?: string, sortDirection?: string, filterRules?: FilterRule[]): Observable<Results<PaperlessDocument>> { - return super.list(page, pageSize, sortField, sortDirection, this.filterRulesToQueryParams(filterRules)).pipe( + list(page?: number, pageSize?: number, sortField?: string, sortReverse?: boolean, filterRules?: FilterRule[]): Observable<Results<PaperlessDocument>> { + return super.list(page, pageSize, sortField, sortReverse, this.filterRulesToQueryParams(filterRules)).pipe( map(results => { results.results.forEach(doc => this.addObservablesToDocument(doc)) return results From 381a50394761adf5328d83779d22ab5dc2042d77 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Mon, 14 Dec 2020 19:39:16 +0100 Subject: [PATCH 0108/1708] bugfix --- .../components/document-list/document-list.component.ts | 1 + src-ui/src/app/services/document-list-view.service.ts | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index eb3b89db1..1653b0965 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -76,6 +76,7 @@ export class DocumentListComponent implements OnInit { loadViewConfig(view: PaperlessSavedView) { this.list.load(view) + this.list.reload() } saveViewConfig() { diff --git a/src-ui/src/app/services/document-list-view.service.ts b/src-ui/src/app/services/document-list-view.service.ts index 4fa5e23d9..3353e1d0a 100644 --- a/src-ui/src/app/services/document-list-view.service.ts +++ b/src-ui/src/app/services/document-list-view.service.ts @@ -76,11 +76,10 @@ export class DocumentListViewService { } load(view: PaperlessSavedView) { - this.view.filter_rules = cloneFilterRules(view.filter_rules) - this.view.sort_reverse = view.sort_reverse - this.view.sort_field = view.sort_field + this.documentListView.filter_rules = cloneFilterRules(view.filter_rules) + this.documentListView.sort_reverse = view.sort_reverse + this.documentListView.sort_field = view.sort_field this.saveDocumentListView() - this.reload() } clear() { From 958acd8a36612b0495ea6eade5e37b019a106390 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Mon, 14 Dec 2020 20:20:35 +0100 Subject: [PATCH 0109/1708] imports --- src-ui/src/app/components/common/input/abstract-input.ts | 4 ++-- .../components/common/input/date-time/date-time.component.ts | 1 - .../app/components/common/input/select/select.component.ts | 2 +- src-ui/src/app/components/common/input/tags/tags.component.ts | 2 -- src-ui/src/app/components/common/tag/tag.component.ts | 2 +- .../document-card-large/document-card-large.component.ts | 1 - .../document-card-small/document-card-small.component.ts | 1 - .../filter-dropdown/filter-dropdown.component.ts | 2 -- .../correspondent-edit-dialog.component.ts | 2 +- .../document-type-edit-dialog.component.ts | 2 +- .../src/app/components/manage/settings/settings.component.ts | 1 - src-ui/src/app/services/document-list-view.service.ts | 1 - src-ui/src/app/services/rest/abstract-paperless-service.ts | 2 +- src-ui/src/app/services/rest/saved-view.service.ts | 3 +-- 14 files changed, 8 insertions(+), 18 deletions(-) diff --git a/src-ui/src/app/components/common/input/abstract-input.ts b/src-ui/src/app/components/common/input/abstract-input.ts index 318b9de9f..78a4a1b69 100644 --- a/src-ui/src/app/components/common/input/abstract-input.ts +++ b/src-ui/src/app/components/common/input/abstract-input.ts @@ -1,5 +1,5 @@ -import { Component, Directive, forwardRef, Input, OnInit } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Directive, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor } from '@angular/forms'; import { v4 as uuidv4 } from 'uuid'; @Directive() diff --git a/src-ui/src/app/components/common/input/date-time/date-time.component.ts b/src-ui/src/app/components/common/input/date-time/date-time.component.ts index 6a04c5b27..bce208ec8 100644 --- a/src-ui/src/app/components/common/input/date-time/date-time.component.ts +++ b/src-ui/src/app/components/common/input/date-time/date-time.component.ts @@ -1,7 +1,6 @@ import { formatDate } from '@angular/common'; import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { AbstractInputComponent } from '../abstract-input'; @Component({ providers: [{ diff --git a/src-ui/src/app/components/common/input/select/select.component.ts b/src-ui/src/app/components/common/input/select/select.component.ts index e6e02ac87..18f30cf6e 100644 --- a/src-ui/src/app/components/common/input/select/select.component.ts +++ b/src-ui/src/app/components/common/input/select/select.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, forwardRef, Input, Output } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { AbstractInputComponent } from '../abstract-input'; diff --git a/src-ui/src/app/components/common/input/tags/tags.component.ts b/src-ui/src/app/components/common/input/tags/tags.component.ts index 81bd9d470..cca99cc55 100644 --- a/src-ui/src/app/components/common/input/tags/tags.component.ts +++ b/src-ui/src/app/components/common/input/tags/tags.component.ts @@ -1,8 +1,6 @@ -import { ThrowStmt } from '@angular/compiler'; import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { Observable } from 'rxjs'; import { TagEditDialogComponent } from 'src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component'; import { PaperlessTag } from 'src/app/data/paperless-tag'; import { TagService } from 'src/app/services/rest/tag.service'; diff --git a/src-ui/src/app/components/common/tag/tag.component.ts b/src-ui/src/app/components/common/tag/tag.component.ts index c032c51db..0b1186ce0 100644 --- a/src-ui/src/app/components/common/tag/tag.component.ts +++ b/src-ui/src/app/components/common/tag/tag.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; import { TAG_COLOURS, PaperlessTag } from 'src/app/data/paperless-tag'; @Component({ diff --git a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts index ac2fdba27..2e056cc70 100644 --- a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts +++ b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts @@ -1,7 +1,6 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; import { PaperlessDocument } from 'src/app/data/paperless-document'; -import { PaperlessTag } from 'src/app/data/paperless-tag'; import { DocumentService } from 'src/app/services/rest/document.service'; @Component({ diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts index d60552d4f..d87eb4331 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts @@ -1,7 +1,6 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { map } from 'rxjs/operators'; import { PaperlessDocument } from 'src/app/data/paperless-document'; -import { PaperlessTag } from 'src/app/data/paperless-tag'; import { DocumentService } from 'src/app/services/rest/document.service'; @Component({ diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts index a24e7347d..d675e14f1 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts @@ -1,6 +1,4 @@ import { Component, EventEmitter, Input, Output, ElementRef, ViewChild } from '@angular/core'; -import { Observable } from 'rxjs'; -import { Results } from 'src/app/data/results'; import { ObjectWithId } from 'src/app/data/object-with-id'; import { FilterPipe } from 'src/app/pipes/filter.pipe'; import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' diff --git a/src-ui/src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.ts b/src-ui/src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.ts index 855fc159c..bc6b2a823 100644 --- a/src-ui/src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.ts +++ b/src-ui/src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'; diff --git a/src-ui/src/app/components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component.ts b/src-ui/src/app/components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component.ts index 087eede8c..a8052f453 100644 --- a/src-ui/src/app/components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component.ts +++ b/src-ui/src/app/components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'; diff --git a/src-ui/src/app/components/manage/settings/settings.component.ts b/src-ui/src/app/components/manage/settings/settings.component.ts index 8ceee6e03..3f7afe5b3 100644 --- a/src-ui/src/app/components/manage/settings/settings.component.ts +++ b/src-ui/src/app/components/manage/settings/settings.component.ts @@ -1,7 +1,6 @@ import { Component, OnInit } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; import { Title } from '@angular/platform-browser'; -import { map, tap } from 'rxjs/operators'; import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'; import { GENERAL_SETTINGS } from 'src/app/data/storage-keys'; import { DocumentListViewService } from 'src/app/services/document-list-view.service'; diff --git a/src-ui/src/app/services/document-list-view.service.ts b/src-ui/src/app/services/document-list-view.service.ts index 3353e1d0a..7405fcd24 100644 --- a/src-ui/src/app/services/document-list-view.service.ts +++ b/src-ui/src/app/services/document-list-view.service.ts @@ -188,7 +188,6 @@ export class DocumentListViewService { let newPageSize = +localStorage.getItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE) || GENERAL_SETTINGS.DOCUMENT_LIST_SIZE_DEFAULT if (newPageSize != this.currentPageSize) { this.currentPageSize = newPageSize - //this.reload() } } diff --git a/src-ui/src/app/services/rest/abstract-paperless-service.ts b/src-ui/src/app/services/rest/abstract-paperless-service.ts index 396baa1c5..6ec4346ed 100644 --- a/src-ui/src/app/services/rest/abstract-paperless-service.ts +++ b/src-ui/src/app/services/rest/abstract-paperless-service.ts @@ -1,5 +1,5 @@ import { HttpClient, HttpParams } from '@angular/common/http' -import { Observable, of, Subject } from 'rxjs' +import { Observable } from 'rxjs' import { map, publishReplay, refCount } from 'rxjs/operators' import { ObjectWithId } from 'src/app/data/object-with-id' import { Results } from 'src/app/data/results' diff --git a/src-ui/src/app/services/rest/saved-view.service.ts b/src-ui/src/app/services/rest/saved-view.service.ts index 343b1a8f8..14c18b0e2 100644 --- a/src-ui/src/app/services/rest/saved-view.service.ts +++ b/src-ui/src/app/services/rest/saved-view.service.ts @@ -1,7 +1,6 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { Subject } from 'rxjs'; -import { map, tap } from 'rxjs/operators'; +import { tap } from 'rxjs/operators'; import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'; import { AbstractPaperlessService } from './abstract-paperless-service'; From 889fe5890dfb3c939c9b8d6c445920eeefcdf0ba Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Mon, 14 Dec 2020 20:59:18 +0100 Subject: [PATCH 0110/1708] refactored titles --- .../page-header/page-header.component.ts | 22 +++++++++++++------ .../dashboard/dashboard.component.ts | 6 +---- .../document-detail.component.ts | 6 +---- .../document-list/document-list.component.ts | 7 +----- .../correspondent-list.component.ts | 11 ++-------- .../document-type-list.component.ts | 12 +++------- .../components/manage/logs/logs.component.ts | 5 +---- .../manage/settings/settings.component.ts | 11 ++-------- .../manage/tag-list/tag-list.component.ts | 14 +++--------- .../app/components/search/search.component.ts | 5 +---- 10 files changed, 30 insertions(+), 69 deletions(-) diff --git a/src-ui/src/app/components/common/page-header/page-header.component.ts b/src-ui/src/app/components/common/page-header/page-header.component.ts index 93ec3bfb7..153e6bea6 100644 --- a/src-ui/src/app/components/common/page-header/page-header.component.ts +++ b/src-ui/src/app/components/common/page-header/page-header.component.ts @@ -1,21 +1,29 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input } from '@angular/core'; +import { Title } from '@angular/platform-browser'; +import { environment } from 'src/environments/environment'; @Component({ selector: 'app-page-header', templateUrl: './page-header.component.html', styleUrls: ['./page-header.component.scss'] }) -export class PageHeaderComponent implements OnInit { +export class PageHeaderComponent { - constructor() { } + constructor(private titleService: Title) { } + + _title = "" @Input() - title: string = "" + set title(title: string) { + this._title = title + this.titleService.setTitle(`${this.title} - ${environment.appTitle}`) + } + + get title() { + return this._title + } @Input() subTitle: string = "" - ngOnInit(): void { - } - } diff --git a/src-ui/src/app/components/dashboard/dashboard.component.ts b/src-ui/src/app/components/dashboard/dashboard.component.ts index 57744d194..a14ec5e90 100644 --- a/src-ui/src/app/components/dashboard/dashboard.component.ts +++ b/src-ui/src/app/components/dashboard/dashboard.component.ts @@ -1,8 +1,6 @@ import { Component, OnInit } from '@angular/core'; -import { Title } from '@angular/platform-browser'; import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'; import { SavedViewService } from 'src/app/services/rest/saved-view.service'; -import { environment } from 'src/environments/environment'; @Component({ @@ -13,8 +11,7 @@ import { environment } from 'src/environments/environment'; export class DashboardComponent implements OnInit { constructor( - private savedViewService: SavedViewService, - private titleService: Title) { } + private savedViewService: SavedViewService) { } savedViews: PaperlessSavedView[] = [] @@ -23,7 +20,6 @@ export class DashboardComponent implements OnInit { this.savedViewService.listAll().subscribe(results => { this.savedViews = results.results.filter(savedView => savedView.show_on_dashboard) }) - this.titleService.setTitle(`Dashboard - ${environment.appTitle}`) } } diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index 4aac9c769..5fe9f9250 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -1,6 +1,5 @@ import { Component, OnInit } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; -import { Title } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'; @@ -12,7 +11,6 @@ import { OpenDocumentsService } from 'src/app/services/open-documents.service'; import { CorrespondentService } from 'src/app/services/rest/correspondent.service'; import { DocumentTypeService } from 'src/app/services/rest/document-type.service'; import { DocumentService } from 'src/app/services/rest/document.service'; -import { environment } from 'src/environments/environment'; import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'; import { CorrespondentEditDialogComponent } from '../manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component'; import { DocumentTypeEditDialogComponent } from '../manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component'; @@ -56,8 +54,7 @@ export class DocumentDetailComponent implements OnInit { private router: Router, private modalService: NgbModal, private openDocumentService: OpenDocumentsService, - private documentListViewService: DocumentListViewService, - private titleService: Title) { } + private documentListViewService: DocumentListViewService) { } getContentType() { return this.metadata?.has_archive_version ? 'application/pdf' : this.metadata?.original_mime_type @@ -90,7 +87,6 @@ export class DocumentDetailComponent implements OnInit { updateComponent(doc: PaperlessDocument) { this.document = doc - this.titleService.setTitle(`${doc.title} - ${environment.appTitle}`) this.documentsService.getMetadata(doc.id).subscribe(result => { this.metadata = result }) diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index 1653b0965..d31b12e6c 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -1,5 +1,4 @@ import { Component, OnInit, ViewChild } from '@angular/core'; -import { Title } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'; @@ -7,7 +6,6 @@ import { DocumentListViewService } from 'src/app/services/document-list-view.ser import { DOCUMENT_SORT_FIELDS } from 'src/app/services/rest/document.service'; import { SavedViewService } from 'src/app/services/rest/saved-view.service'; import { Toast, ToastService } from 'src/app/services/toast.service'; -import { environment } from 'src/environments/environment'; import { FilterEditorComponent } from '../filter-editor/filter-editor.component'; import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component'; @@ -24,8 +22,7 @@ export class DocumentListComponent implements OnInit { public route: ActivatedRoute, private router: Router, private toastService: ToastService, - public modalService: NgbModal, - private titleService: Title) { } + public modalService: NgbModal) { } @ViewChild("filterEditor") private filterEditor: FilterEditorComponent @@ -62,12 +59,10 @@ export class DocumentListComponent implements OnInit { } this.list.savedView = view - this.titleService.setTitle(`${this.list.savedView.name} - ${environment.appTitle}`) this.list.reload() }) } else { this.list.savedView = null - this.titleService.setTitle(`Documents - ${environment.appTitle}`) this.list.reload() } }) diff --git a/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts b/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts index 11027c60f..effae2826 100644 --- a/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts +++ b/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts @@ -1,9 +1,7 @@ import { Component, OnInit } from '@angular/core'; -import { Title } from '@angular/platform-browser'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'; import { CorrespondentService } from 'src/app/services/rest/correspondent.service'; -import { environment } from 'src/environments/environment'; import { GenericListComponent } from '../generic-list/generic-list.component'; import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog/correspondent-edit-dialog.component'; @@ -12,9 +10,9 @@ import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog/co templateUrl: './correspondent-list.component.html', styleUrls: ['./correspondent-list.component.scss'] }) -export class CorrespondentListComponent extends GenericListComponent<PaperlessCorrespondent> implements OnInit { +export class CorrespondentListComponent extends GenericListComponent<PaperlessCorrespondent> { - constructor(correspondentsService: CorrespondentService, modalService: NgbModal, private titleService: Title) { + constructor(correspondentsService: CorrespondentService, modalService: NgbModal,) { super(correspondentsService,modalService,CorrespondentEditDialogComponent) } @@ -22,9 +20,4 @@ export class CorrespondentListComponent extends GenericListComponent<PaperlessCo return `correspondent '${object.name}'` } - ngOnInit(): void { - super.ngOnInit() - this.titleService.setTitle(`Correspondents - ${environment.appTitle}`) - } - } diff --git a/src-ui/src/app/components/manage/document-type-list/document-type-list.component.ts b/src-ui/src/app/components/manage/document-type-list/document-type-list.component.ts index 316024514..16cdd88a9 100644 --- a/src-ui/src/app/components/manage/document-type-list/document-type-list.component.ts +++ b/src-ui/src/app/components/manage/document-type-list/document-type-list.component.ts @@ -1,9 +1,7 @@ -import { Component, OnInit } from '@angular/core'; -import { Title } from '@angular/platform-browser'; +import { Component } from '@angular/core'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'; import { DocumentTypeService } from 'src/app/services/rest/document-type.service'; -import { environment } from 'src/environments/environment'; import { GenericListComponent } from '../generic-list/generic-list.component'; import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog/document-type-edit-dialog.component'; @@ -12,9 +10,9 @@ import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog/doc templateUrl: './document-type-list.component.html', styleUrls: ['./document-type-list.component.scss'] }) -export class DocumentTypeListComponent extends GenericListComponent<PaperlessDocumentType> implements OnInit { +export class DocumentTypeListComponent extends GenericListComponent<PaperlessDocumentType> { - constructor(service: DocumentTypeService, modalService: NgbModal, private titleService: Title) { + constructor(service: DocumentTypeService, modalService: NgbModal) { super(service, modalService, DocumentTypeEditDialogComponent) } @@ -22,8 +20,4 @@ export class DocumentTypeListComponent extends GenericListComponent<PaperlessDoc return `document type '${object.name}'` } - ngOnInit(): void { - super.ngOnInit() - this.titleService.setTitle(`Document types - ${environment.appTitle}`) - } } diff --git a/src-ui/src/app/components/manage/logs/logs.component.ts b/src-ui/src/app/components/manage/logs/logs.component.ts index 131f91f9c..b131796ee 100644 --- a/src-ui/src/app/components/manage/logs/logs.component.ts +++ b/src-ui/src/app/components/manage/logs/logs.component.ts @@ -1,8 +1,6 @@ import { Component, OnInit } from '@angular/core'; -import { Title } from '@angular/platform-browser'; import { LOG_LEVELS, LOG_LEVEL_INFO, PaperlessLog } from 'src/app/data/paperless-log'; import { LogService } from 'src/app/services/rest/log.service'; -import { environment } from 'src/environments/environment'; @Component({ selector: 'app-logs', @@ -11,14 +9,13 @@ import { environment } from 'src/environments/environment'; }) export class LogsComponent implements OnInit { - constructor(private logService: LogService, private titleService: Title) { } + constructor(private logService: LogService) { } logs: PaperlessLog[] = [] level: number = LOG_LEVEL_INFO ngOnInit(): void { this.reload() - this.titleService.setTitle(`Logs - ${environment.appTitle}`) } reload() { diff --git a/src-ui/src/app/components/manage/settings/settings.component.ts b/src-ui/src/app/components/manage/settings/settings.component.ts index 3f7afe5b3..571f60620 100644 --- a/src-ui/src/app/components/manage/settings/settings.component.ts +++ b/src-ui/src/app/components/manage/settings/settings.component.ts @@ -1,18 +1,16 @@ import { Component, OnInit } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; -import { Title } from '@angular/platform-browser'; import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'; import { GENERAL_SETTINGS } from 'src/app/data/storage-keys'; import { DocumentListViewService } from 'src/app/services/document-list-view.service'; import { SavedViewService } from 'src/app/services/rest/saved-view.service'; -import { environment } from 'src/environments/environment'; @Component({ selector: 'app-settings', templateUrl: './settings.component.html', styleUrls: ['./settings.component.scss'] }) -export class SettingsComponent implements OnInit { +export class SettingsComponent { settingsForm = new FormGroup({ 'documentListItemPerPage': new FormControl(+localStorage.getItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE) || GENERAL_SETTINGS.DOCUMENT_LIST_SIZE_DEFAULT) @@ -20,14 +18,9 @@ export class SettingsComponent implements OnInit { constructor( public savedViewService: SavedViewService, - private documentListViewService: DocumentListViewService, - private titleService: Title + private documentListViewService: DocumentListViewService ) { } - ngOnInit(): void { - this.titleService.setTitle(`Settings - ${environment.appTitle}`) - } - deleteSavedView(savedView: PaperlessSavedView) { this.savedViewService.delete(savedView).subscribe(() => {}) } diff --git a/src-ui/src/app/components/manage/tag-list/tag-list.component.ts b/src-ui/src/app/components/manage/tag-list/tag-list.component.ts index efbe11321..32093e0a8 100644 --- a/src-ui/src/app/components/manage/tag-list/tag-list.component.ts +++ b/src-ui/src/app/components/manage/tag-list/tag-list.component.ts @@ -1,9 +1,7 @@ -import { Component, OnInit } from '@angular/core'; -import { Title } from '@angular/platform-browser'; +import { Component } from '@angular/core'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { TAG_COLOURS, PaperlessTag } from 'src/app/data/paperless-tag'; import { TagService } from 'src/app/services/rest/tag.service'; -import { environment } from 'src/environments/environment'; import { GenericListComponent } from '../generic-list/generic-list.component'; import { TagEditDialogComponent } from './tag-edit-dialog/tag-edit-dialog.component'; @@ -12,18 +10,12 @@ import { TagEditDialogComponent } from './tag-edit-dialog/tag-edit-dialog.compon templateUrl: './tag-list.component.html', styleUrls: ['./tag-list.component.scss'] }) -export class TagListComponent extends GenericListComponent<PaperlessTag> implements OnInit { +export class TagListComponent extends GenericListComponent<PaperlessTag> { - constructor(tagService: TagService, modalService: NgbModal, private titleService: Title) { + constructor(tagService: TagService, modalService: NgbModal) { super(tagService, modalService, TagEditDialogComponent) } - - ngOnInit(): void { - super.ngOnInit() - this.titleService.setTitle(`Tags - ${environment.appTitle}`) - } - getColor(id) { return TAG_COLOURS.find(c => c.id == id) } diff --git a/src-ui/src/app/components/search/search.component.ts b/src-ui/src/app/components/search/search.component.ts index 3371debd2..de8b4652f 100644 --- a/src-ui/src/app/components/search/search.component.ts +++ b/src-ui/src/app/components/search/search.component.ts @@ -1,9 +1,7 @@ import { Component, OnInit } from '@angular/core'; -import { Title } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; import { SearchHit } from 'src/app/data/search-result'; import { SearchService } from 'src/app/services/rest/search.service'; -import { environment } from 'src/environments/environment'; @Component({ selector: 'app-search', @@ -28,7 +26,7 @@ export class SearchComponent implements OnInit { errorMessage: string - constructor(private searchService: SearchService, private route: ActivatedRoute, private router: Router, private titleService: Title) { } + constructor(private searchService: SearchService, private route: ActivatedRoute, private router: Router) { } ngOnInit(): void { this.route.queryParamMap.subscribe(paramMap => { @@ -36,7 +34,6 @@ export class SearchComponent implements OnInit { this.searching = true this.currentPage = 1 this.loadPage() - this.titleService.setTitle(`Search: ${this.query} - ${environment.appTitle}`) }) } From de87efc2912ad058dbc8ca87fbd795feb9467bf5 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Mon, 14 Dec 2020 21:14:33 +0100 Subject: [PATCH 0111/1708] confirmation messages --- .../components/document-list/document-list.component.ts | 6 ++++-- .../app/components/manage/settings/settings.component.ts | 8 ++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index d31b12e6c..4b711f9dc 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -84,15 +84,17 @@ export class DocumentListComponent implements OnInit { saveViewConfigAs() { let modal = this.modalService.open(SaveViewConfigDialogComponent, {backdrop: 'static'}) modal.componentInstance.saveClicked.subscribe(formValue => { - this.savedViewService.create({ + let savedView = { name: formValue.name, show_on_dashboard: formValue.showOnDashboard, show_in_sidebar: formValue.showInSideBar, filter_rules: this.list.filterRules, sort_reverse: this.list.sortReverse, sort_field: this.list.sortField - }).subscribe(() => { + } + this.savedViewService.create(savedView).subscribe(() => { modal.close() + this.toastService.showToast(Toast.make("Information", `View "${savedView.name}" created successfully.`)) }) }) } diff --git a/src-ui/src/app/components/manage/settings/settings.component.ts b/src-ui/src/app/components/manage/settings/settings.component.ts index 571f60620..08275bbb2 100644 --- a/src-ui/src/app/components/manage/settings/settings.component.ts +++ b/src-ui/src/app/components/manage/settings/settings.component.ts @@ -4,6 +4,7 @@ import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'; import { GENERAL_SETTINGS } from 'src/app/data/storage-keys'; import { DocumentListViewService } from 'src/app/services/document-list-view.service'; import { SavedViewService } from 'src/app/services/rest/saved-view.service'; +import { Toast, ToastService } from 'src/app/services/toast.service'; @Component({ selector: 'app-settings', @@ -18,11 +19,14 @@ export class SettingsComponent { constructor( public savedViewService: SavedViewService, - private documentListViewService: DocumentListViewService + private documentListViewService: DocumentListViewService, + private toastService: ToastService ) { } deleteSavedView(savedView: PaperlessSavedView) { - this.savedViewService.delete(savedView).subscribe(() => {}) + this.savedViewService.delete(savedView).subscribe(() => { + this.toastService.showToast(Toast.make("Information", `Saved view "${savedView.name} deleted.`)) + }) } saveSettings() { From cf619d9d31aed8b7cbf9175e3b97bcbf32f3b58e Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Mon, 14 Dec 2020 22:20:28 +0100 Subject: [PATCH 0112/1708] typing --- .../filter-editor/filter-editor.component.ts | 36 +++++++++---------- src-ui/src/app/data/filter-rule.ts | 2 +- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/filter-editor/filter-editor.component.ts index c08b12bee..8d30e3d60 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.ts @@ -40,17 +40,17 @@ export class FilterEditorComponent implements OnInit, OnDestroy { 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)) + 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)) + 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)) + return this.documentTypes?.filter(dt => documentTypeRules.find(dtr => +dtr.value == dt.id)) } get titleFilter() { @@ -100,7 +100,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy { let filterRuleType = FILTER_RULE_TYPES.find(t => t.id == filterRuleTypeID) - let existingRule = this.filterRules.find(rule => rule.rule_type == filterRuleTypeID && rule.value == value) + 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) { @@ -108,10 +108,10 @@ export class FilterEditorComponent implements OnInit, OnDestroy { 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}) + 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 + existingRuleOfSameType.value = value?.toString() } this.applyFilters() } @@ -169,36 +169,36 @@ export class FilterEditorComponent implements OnInit, OnDestroy { get dateCreatedBefore(): NgbDateStruct { let createdBeforeRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_CREATED_BEFORE) return createdBeforeRule ? { - year: createdBeforeRule.value.substring(0,4), - month: createdBeforeRule.value.substring(5,7), - day: createdBeforeRule.value.substring(8,10) + year: +createdBeforeRule.value.substring(0,4), + month: +createdBeforeRule.value.substring(5,7), + day: +createdBeforeRule.value.substring(8,10) } : undefined } get dateCreatedAfter(): NgbDateStruct { let createdAfterRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_CREATED_AFTER) return createdAfterRule ? { - year: createdAfterRule.value.substring(0,4), - month: createdAfterRule.value.substring(5,7), - day: createdAfterRule.value.substring(8,10) + year: +createdAfterRule.value.substring(0,4), + month: +createdAfterRule.value.substring(5,7), + day: +createdAfterRule.value.substring(8,10) } : undefined } get dateAddedBefore(): NgbDateStruct { let addedBeforeRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_ADDED_BEFORE) return addedBeforeRule ? { - year: addedBeforeRule.value.substring(0,4), - month: addedBeforeRule.value.substring(5,7), - day: addedBeforeRule.value.substring(8,10) + year: +addedBeforeRule.value.substring(0,4), + month: +addedBeforeRule.value.substring(5,7), + day: +addedBeforeRule.value.substring(8,10) } : undefined } get dateAddedAfter(): NgbDateStruct { let addedAfterRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_ADDED_AFTER) return addedAfterRule ? { - year: addedAfterRule.value.substring(0,4), - month: addedAfterRule.value.substring(5,7), - day: addedAfterRule.value.substring(8,10) + year: +addedAfterRule.value.substring(0,4), + month: +addedAfterRule.value.substring(5,7), + day: +addedAfterRule.value.substring(8,10) } : undefined } diff --git a/src-ui/src/app/data/filter-rule.ts b/src-ui/src/app/data/filter-rule.ts index a0c6f0086..82d8498f3 100644 --- a/src-ui/src/app/data/filter-rule.ts +++ b/src-ui/src/app/data/filter-rule.ts @@ -12,5 +12,5 @@ export function cloneFilterRules(filterRules: FilterRule[]): FilterRule[] { export interface FilterRule { rule_type: number - value: any + value: string } \ No newline at end of file From 45848f5e348af304c46d50405a3458403534d0c8 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Mon, 14 Dec 2020 22:46:50 +0100 Subject: [PATCH 0113/1708] removed manual date formatting/parsing --- .../filter-editor/filter-editor.component.ts | 32 +++++-------------- 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/filter-editor/filter-editor.component.ts index 8d30e3d60..9822c7db3 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.ts @@ -4,7 +4,7 @@ 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 { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; +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'; @@ -21,7 +21,8 @@ export class FilterEditorComponent implements OnInit, OnDestroy { constructor( private documentTypeService: DocumentTypeService, private tagService: TagService, - private correspondentService: CorrespondentService + private correspondentService: CorrespondentService, + private dateParser: NgbDateParserFormatter ) { } tags: PaperlessTag[] = [] @@ -76,7 +77,6 @@ export class FilterEditorComponent implements OnInit, OnDestroy { debounceTime(400), distinctUntilChanged() ).subscribe(title => { - this.setTitleRule(title) }) } @@ -168,38 +168,22 @@ export class FilterEditorComponent implements OnInit, OnDestroy { get dateCreatedBefore(): NgbDateStruct { let createdBeforeRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_CREATED_BEFORE) - return createdBeforeRule ? { - year: +createdBeforeRule.value.substring(0,4), - month: +createdBeforeRule.value.substring(5,7), - day: +createdBeforeRule.value.substring(8,10) - } : undefined + return createdBeforeRule ? this.dateParser.parse(createdBeforeRule.value) : null } get dateCreatedAfter(): NgbDateStruct { let createdAfterRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_CREATED_AFTER) - return createdAfterRule ? { - year: +createdAfterRule.value.substring(0,4), - month: +createdAfterRule.value.substring(5,7), - day: +createdAfterRule.value.substring(8,10) - } : undefined + return createdAfterRule ? this.dateParser.parse(createdAfterRule.value) : null } get dateAddedBefore(): NgbDateStruct { let addedBeforeRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_ADDED_BEFORE) - return addedBeforeRule ? { - year: +addedBeforeRule.value.substring(0,4), - month: +addedBeforeRule.value.substring(5,7), - day: +addedBeforeRule.value.substring(8,10) - } : undefined + return addedBeforeRule ? this.dateParser.parse(addedBeforeRule.value) : null } get dateAddedAfter(): NgbDateStruct { let addedAfterRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_ADDED_AFTER) - return addedAfterRule ? { - year: +addedAfterRule.value.substring(0,4), - month: +addedAfterRule.value.substring(5,7), - day: +addedAfterRule.value.substring(8,10) - } : undefined + return addedAfterRule ? this.dateParser.parse(addedAfterRule.value) : null } setDateCreatedBefore(date?: NgbDateStruct) { @@ -225,7 +209,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy { setDateFilter(date: NgbDateStruct, dateRuleTypeID: number) { let filterRules = this.filterRules let existingRule = filterRules.find(rule => rule.rule_type == dateRuleTypeID) - let newValue = `${date.year}-${date.month.toString().padStart(2,'0')}-${date.day.toString().padStart(2,'0')}` // YYYY-MM-DD + let newValue = this.dateParser.format(date) if (existingRule) { existingRule.value = newValue From 4ed56e460338cf4000dfbf47af9a4cf087851b24 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Tue, 15 Dec 2020 00:00:40 +0100 Subject: [PATCH 0114/1708] fix --- .../filter-dropdown-date/filter-dropdown-date.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts index fbe9bdc14..55beeb7f4 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts @@ -58,7 +58,7 @@ export class FilterDropdownDateComponent { dpAfterElRef.nativeElement.value = dateString } else if (dateBeforeChange && dateBeforeChange.currentValue) { let dateBeforeDate = dateBeforeChange.currentValue as NgbDateStruct - dateString = `${dateBeforeChange.currentValue.year}-${dateBeforeChange.currentValue.month.toString().padStart(2,'0')}-${dateBeforeChange.currentValue.day.toString().padStart(2,'0')}` + dateString = `${dateBeforeDate.year}-${dateBeforeDate.month.toString().padStart(2,'0')}-${dateBeforeDate.day.toString().padStart(2,'0')}` dpBeforeElRef.nativeElement.value = dateString } else { dpAfterElRef.nativeElement.value = dateString From 533be7e96e2a4c919304e914c60127f742d8bbd6 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Tue, 15 Dec 2020 00:30:36 +0100 Subject: [PATCH 0115/1708] better highlight of active filters --- .../filter-dropdown-date.component.html | 7 +------ .../filter-dropdown/filter-dropdown.component.html | 7 +------ .../components/filter-editor/filter-editor.component.html | 2 +- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html index c4befd701..ad292b182 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html @@ -1,10 +1,5 @@ <div class="btn-group" ngbDropdown role="group"> - <button class="btn btn-outline-primary btn-sm" id="dropdown{{title}}" ngbDropdownToggle> - <ng-container *ngIf="dateBefore || dateAfter"> - <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check-circle-fill text-secondary" viewBox="0 0 16 16"> - <path fill-rule="evenodd" d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/> - </svg> - </ng-container> + <button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="dateBefore || dateAfter ? 'btn-primary' : 'btn-outline-primary'"> {{title}} </button> <div class="dropdown-menu date-filter shadow pt-0" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html index 975e96ec2..5de9228bd 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html @@ -1,10 +1,5 @@ <div class="btn-group" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #filterDropdown="ngbDropdown"> - <button class="btn btn-outline-primary btn-sm" id="dropdown{{title}}" ngbDropdownToggle> - <ng-container *ngIf="itemsSelected?.length > 0"> - <div class="badge bg-secondary text-light rounded-pill ml-auto"> - {{itemsSelected?.length}} - </div> - </ng-container> + <button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="itemsSelected?.length > 0 ? 'btn-primary' : 'btn-outline-primary'"> {{title}} </button> <div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.html b/src-ui/src/app/components/filter-editor/filter-editor.component.html index b50ed53e3..6e64264db 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.html +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.html @@ -8,7 +8,7 @@ <app-filter-dropdown class="col-auto" [items]="tags" [itemsSelected]="selectedTags" title="Tags" (toggle)="toggleTag($event.id)"></app-filter-dropdown> <app-filter-dropdown class="col-auto" [items]="correspondents" [itemsSelected]="selectedCorrespondents" title="Correspondents" (toggle)="toggleCorrespondent($event.id)"></app-filter-dropdown> - <app-filter-dropdown class="col-auto" [items]="documentTypes" [itemsSelected]="selectedDocumentTypes" title="Document Types" (toggle)="toggleDocumentType($event.id)"></app-filter-dropdown> + <app-filter-dropdown class="col-auto" [items]="documentTypes" [itemsSelected]="selectedDocumentTypes" title="Document types" (toggle)="toggleDocumentType($event.id)"></app-filter-dropdown> <app-filter-dropdown-date class="col-auto" [dateBefore]="dateCreatedBefore" [dateAfter]="dateCreatedAfter" title="Created" (dateBeforeSet)="onDateCreatedBeforeSet($event)" (dateAfterSet)="onDateCreatedAfterSet($event)"></app-filter-dropdown-date> <app-filter-dropdown-date class="col-auto" [dateBefore]="dateAddedBefore" [dateAfter]="dateAddedAfter" title="Added" (dateBeforeSet)="onDateAddedBeforeSet($event)" (dateAfterSet)="onDateAddedAfterSet($event)"></app-filter-dropdown-date> From 67d03c11b9a73d8ec80a031ce0d2e0740edf33b6 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Tue, 15 Dec 2020 00:48:06 +0100 Subject: [PATCH 0116/1708] fixed the date selection dropdowns. - They still contain that ugly hack. --- .../filter-dropdown-date.component.html | 25 ++++------------- .../filter-dropdown-date.component.ts | 27 ++++++++++--------- .../filter-editor.component.html | 4 +-- .../filter-editor/filter-editor.component.ts | 21 +++++---------- 4 files changed, 29 insertions(+), 48 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html index ad292b182..6f6a42fe2 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html @@ -4,23 +4,16 @@ </button> <div class="dropdown-menu date-filter shadow pt-0" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> <div class="list-group list-group-flush"> + <button class="list-group-item small list-goup list-group-item-action d-flex p-2 pl-3" (click)="clear()">Clear</button> <button *ngFor="let range of [7, 30, 'month', 'year']" class="list-group-item small list-goup list-group-item-action d-flex p-2 pl-3" role="menuitem" (click)="setDateQuickFilter(range)"> <ng-container *ngIf="isStringRange(range)">This </ng-container> {{ range }} <ng-container *ngIf="!isStringRange(range)"> days</ng-container> </button> <div class="list-group-item d-flex flex-column align-items-start" role="menuitem"> - <div class="mb-2 d-flex flex-row w-100 justify-content-between small"> - <div>Before</div> - <a *ngIf="dateBefore" class="btn btn-link p-0 m-0" (click)="clearBefore()"> - <svg width="0.8em" height="0.8em" viewBox="0 0 16 16" class="bi bi-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> - <path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/> - </svg> - <small>Clear</small> - </a> - </div> + <div>Before</div> <div class="input-group input-group-sm"> - <input class="form-control" type="text" placeholder="yyyy-mm-dd" name="before" [(ngModel)]="_dateBefore" [maxDate]="this._maxDate" ngbDatepicker (dateSelect)="onDateSelected($event)" #dpBefore="ngbDatepicker"> + <input class="form-control" type="text" placeholder="yyyy-mm-dd" name="before" [(ngModel)]="_dateBefore" [maxDate]="this._maxDate" ngbDatepicker (dateSelect)="onBeforeSelected($event)" #dpBefore="ngbDatepicker"> <div class="input-group-append"> <button class="btn btn-outline-secondary btn-sm" (click)="dpBefore.toggle()" type="button"> <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-calendar-date" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> @@ -32,17 +25,9 @@ </div> </div> <div class="list-group-item d-flex flex-column align-items-start" role="menuitem"> - <div class="mb-2 d-flex flex-row w-100 justify-content-between small"> - <div>After</div> - <a *ngIf="dateAfter" class="btn btn-link p-0 m-0" (click)="clearAfter()"> - <svg width="0.8em" height="0.8em" viewBox="0 0 16 16" class="bi bi-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> - <path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/> - </svg> - <small>Clear</small> - </a> - </div> + <div>After</div> <div class="input-group input-group-sm"> - <input class="form-control form-control-sm" type="text" placeholder="yyyy-mm-dd" name="after" [(ngModel)]="_dateAfter" [maxDate]="this._maxDate" ngbDatepicker (dateSelect)="onDateSelected($event)" #dpAfter="ngbDatepicker"> + <input class="form-control form-control-sm" type="text" placeholder="yyyy-mm-dd" name="after" [(ngModel)]="_dateAfter" [maxDate]="this._maxDate" ngbDatepicker (dateSelect)="onAfterSelected($event)" #dpAfter="ngbDatepicker"> <div class="input-group-append"> <button class="btn btn-outline-secondary btn-sm" (click)="dpAfter.toggle()" type="button"> <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-calendar-date" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts index 55beeb7f4..806027f9c 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts @@ -1,6 +1,13 @@ import { Component, EventEmitter, Input, Output, ElementRef, ViewChild, SimpleChange } from '@angular/core'; import { NgbDate, NgbDateStruct, NgbDatepicker } from '@ng-bootstrap/ng-bootstrap'; + +export interface DateSelection { + before?: NgbDateStruct + after?: NgbDateStruct +} + + @Component({ selector: 'app-filter-dropdown-date', templateUrl: './filter-dropdown-date.component.html', @@ -18,10 +25,7 @@ export class FilterDropdownDateComponent { title: string @Output() - dateBeforeSet = new EventEmitter() - - @Output() - dateAfterSet = new EventEmitter() + datesSet = new EventEmitter<DateSelection>() @ViewChild('dpAfter') dpAfter: NgbDatepicker @ViewChild('dpBefore') dpBefore: NgbDatepicker @@ -88,19 +92,18 @@ export class FilterDropdownDateComponent { break } this._dateAfter = newDate - this.onDateSelected(this._dateAfter) + this.datesSet.emit({after: newDate, before: null}) } - onDateSelected(date:NgbDateStruct) { - let emitter = this._dateAfter && NgbDate.from(this._dateAfter).equals(date) ? this.dateAfterSet : this.dateBeforeSet - emitter.emit(date) + onBeforeSelected(date: NgbDateStruct) { + this.datesSet.emit({after: this._dateAfter, before: date}) } - clearAfter() { - this.dateAfterSet.next() + onAfterSelected(date: NgbDateStruct) { + this.datesSet.emit({after: date, before: this._dateBefore}) } - clearBefore() { - this.dateBeforeSet.next() + clear() { + this.datesSet.emit({after: null, before: null}) } } diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.html b/src-ui/src/app/components/filter-editor/filter-editor.component.html index 6e64264db..80f10407c 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.html +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.html @@ -10,8 +10,8 @@ <app-filter-dropdown class="col-auto" [items]="correspondents" [itemsSelected]="selectedCorrespondents" title="Correspondents" (toggle)="toggleCorrespondent($event.id)"></app-filter-dropdown> <app-filter-dropdown class="col-auto" [items]="documentTypes" [itemsSelected]="selectedDocumentTypes" title="Document types" (toggle)="toggleDocumentType($event.id)"></app-filter-dropdown> - <app-filter-dropdown-date class="col-auto" [dateBefore]="dateCreatedBefore" [dateAfter]="dateCreatedAfter" title="Created" (dateBeforeSet)="onDateCreatedBeforeSet($event)" (dateAfterSet)="onDateCreatedAfterSet($event)"></app-filter-dropdown-date> - <app-filter-dropdown-date class="col-auto" [dateBefore]="dateAddedBefore" [dateAfter]="dateAddedAfter" title="Added" (dateBeforeSet)="onDateAddedBeforeSet($event)" (dateAfterSet)="onDateAddedAfterSet($event)"></app-filter-dropdown-date> + <app-filter-dropdown-date class="col-auto" [dateBefore]="dateCreatedBefore" [dateAfter]="dateCreatedAfter" title="Created" (datesSet)="onDatesCreatedSet($event)"></app-filter-dropdown-date> + <app-filter-dropdown-date class="col-auto" [dateBefore]="dateAddedBefore" [dateAfter]="dateAddedAfter" title="Added" (datesSet)="onDatesAddedSet($event)"></app-filter-dropdown-date> <button class="btn btn-link btn-sm" [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"> diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/filter-editor/filter-editor.component.ts index 9822c7db3..f98b9517f 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.ts @@ -10,6 +10,7 @@ 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', @@ -146,23 +147,15 @@ export class FilterEditorComponent implements OnInit, OnDestroy { // Date handling - onDateCreatedBeforeSet(date: NgbDateStruct) { - this.setDateCreatedBefore(date) + onDatesCreatedSet(dates: DateSelection) { + this.setDateCreatedBefore(dates.before) + this.setDateCreatedAfter(dates.after) this.applyFilters() } - onDateCreatedAfterSet(date: NgbDateStruct) { - this.setDateCreatedAfter(date) - this.applyFilters() - } - - onDateAddedBeforeSet(date: NgbDateStruct) { - this.setDateAddedBefore(date) - this.applyFilters() - } - - onDateAddedAfterSet(date: NgbDateStruct) { - this.setDateAddedAfter(date) + onDatesAddedSet(dates: DateSelection) { + this.setDateAddedBefore(dates.before) + this.setDateAddedAfter(dates.after) this.applyFilters() } From ff71b048483b54f237d079d1efad19558237e422 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Tue, 15 Dec 2020 02:35:04 +0100 Subject: [PATCH 0117/1708] editable saved views --- .../manage/settings/settings.component.html | 47 ++++++++++++------- .../manage/settings/settings.component.ts | 37 +++++++++++++-- .../rest/abstract-paperless-service.ts | 8 +++- .../app/services/rest/saved-view.service.ts | 7 +++ src/documents/serialisers.py | 12 +++-- 5 files changed, 84 insertions(+), 27 deletions(-) diff --git a/src-ui/src/app/components/manage/settings/settings.component.html b/src-ui/src/app/components/manage/settings/settings.component.html index 73e4f8194..f71f12238 100644 --- a/src-ui/src/app/components/manage/settings/settings.component.html +++ b/src-ui/src/app/components/manage/settings/settings.component.html @@ -34,24 +34,35 @@ <a ngbNavLink>Saved views</a> <ng-template ngbNavContent> - <table class="table table-borderless table-sm"> - <thead> - <tr> - <th scope="col">Title</th> - <th scope="col">Show in dashboard</th> - <th scope="col">Show in sidebar</th> - <th scope="col">Actions</th> - </tr> - </thead> - <tbody> - <tr *ngFor="let view of savedViewService.allViews"> - <td>{{ view.name }}</td> - <td>{{ view.show_on_dashboard | yesno }}</td> - <td>{{ view.show_in_sidebar | yesno }}</td> - <td><button type="button" class="btn btn-sm btn-outline-danger" (click)="deleteSavedView(view)">Delete</button></td> - </tr> - </tbody> - </table> + <div formGroupName="savedViews"> + + <div *ngFor="let view of savedViews" [formGroupName]="view.id" class="form-row"> + <div class="form-group col-4 mr-3"> + <label for="name_{{view.id}}">Name</label> + <input type="text" class="form-control" formControlName="name" id="name_{{view.id}}"> + </div> + + <div class="form-group col-auto mr-3"> + <label for="show_on_dashboard_{{view.id}}">Appears on</label> + <div class="custom-control custom-switch"> + <input type="checkbox" class="custom-control-input" id="show_on_dashboard_{{view.id}}" formControlName="show_on_dashboard"> + <label class="custom-control-label" for="show_on_dashboard_{{view.id}}">Show on dashboard</label> + </div> + <div class="custom-control custom-switch"> + <input type="checkbox" class="custom-control-input" id="show_in_sidebar_{{view.id}}" formControlName="show_in_sidebar"> + <label class="custom-control-label" for="show_in_sidebar_{{view.id}}">Show in sidebar</label> + </div> + </div> + + <div class="form-group col-auto"> + <label for="name_{{view.id}}">Actions</label> + <button type="button" class="btn btn-sm btn-outline-danger form-control" (click)="deleteSavedView(view)">Delete</button> + </div> + </div> + + <div *ngIf="savedViews.length == 0">No saved views defined.</div> + + </div> </ng-template> </li> diff --git a/src-ui/src/app/components/manage/settings/settings.component.ts b/src-ui/src/app/components/manage/settings/settings.component.ts index 08275bbb2..41bb21156 100644 --- a/src-ui/src/app/components/manage/settings/settings.component.ts +++ b/src-ui/src/app/components/manage/settings/settings.component.ts @@ -11,10 +11,13 @@ import { Toast, ToastService } from 'src/app/services/toast.service'; templateUrl: './settings.component.html', styleUrls: ['./settings.component.scss'] }) -export class SettingsComponent { +export class SettingsComponent implements OnInit { + + savedViewGroup = new FormGroup({}) settingsForm = new FormGroup({ - 'documentListItemPerPage': new FormControl(+localStorage.getItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE) || GENERAL_SETTINGS.DOCUMENT_LIST_SIZE_DEFAULT) + 'documentListItemPerPage': new FormControl(+localStorage.getItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE) || GENERAL_SETTINGS.DOCUMENT_LIST_SIZE_DEFAULT), + 'savedViews': this.savedViewGroup }) constructor( @@ -23,14 +26,40 @@ export class SettingsComponent { private toastService: ToastService ) { } + savedViews: PaperlessSavedView[] + + ngOnInit() { + this.savedViewService.listAll().subscribe(r => { + this.savedViews = r.results + for (let view of this.savedViews) { + this.savedViewGroup.addControl(view.id.toString(), new FormGroup({ + "id": new FormControl(view.id), + "name": new FormControl(view.name), + "show_on_dashboard": new FormControl(view.show_on_dashboard), + "show_in_sidebar": new FormControl(view.show_in_sidebar) + })) + } + }) + } + deleteSavedView(savedView: PaperlessSavedView) { this.savedViewService.delete(savedView).subscribe(() => { + this.savedViewGroup.removeControl(savedView.id.toString()) + this.savedViews.splice(this.savedViews.indexOf(savedView), 1) this.toastService.showToast(Toast.make("Information", `Saved view "${savedView.name} deleted.`)) }) } saveSettings() { - localStorage.setItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE, this.settingsForm.value.documentListItemPerPage) - this.documentListViewService.updatePageSize() + let x = [] + for (let id in this.savedViewGroup.value) { + x.push(this.savedViewGroup.value[id]) + } + this.savedViewService.patchMany(x).subscribe(s => { + this.toastService.showToast(Toast.make("Information", "Settings saved successfully.")) + localStorage.setItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE, this.settingsForm.value.documentListItemPerPage) + this.documentListViewService.updatePageSize() + }) + } } diff --git a/src-ui/src/app/services/rest/abstract-paperless-service.ts b/src-ui/src/app/services/rest/abstract-paperless-service.ts index 6ec4346ed..93e1a0c85 100644 --- a/src-ui/src/app/services/rest/abstract-paperless-service.ts +++ b/src-ui/src/app/services/rest/abstract-paperless-service.ts @@ -92,4 +92,10 @@ export abstract class AbstractPaperlessService<T extends ObjectWithId> { this._listAll = null return this.http.put<T>(this.getResourceUrl(o.id), o) } -} \ No newline at end of file + + patch(o: T): Observable<T> { + this._listAll = null + return this.http.patch<T>(this.getResourceUrl(o.id), o) + } + +} diff --git a/src-ui/src/app/services/rest/saved-view.service.ts b/src-ui/src/app/services/rest/saved-view.service.ts index 14c18b0e2..9a81e01e5 100644 --- a/src-ui/src/app/services/rest/saved-view.service.ts +++ b/src-ui/src/app/services/rest/saved-view.service.ts @@ -1,5 +1,6 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; +import { combineLatest, Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'; import { AbstractPaperlessService } from './abstract-paperless-service'; @@ -44,6 +45,12 @@ export class SavedViewService extends AbstractPaperlessService<PaperlessSavedVie ) } + patchMany(objects: PaperlessSavedView[]): Observable<PaperlessSavedView[]> { + return combineLatest(objects.map(o => super.patch(o))).pipe( + tap(() => this.reload()) + ) + } + delete(o: PaperlessSavedView) { return super.delete(o).pipe( tap(() => this.reload()) diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 43b5e5992..2def07fdd 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -159,11 +159,15 @@ class SavedViewSerializer(serializers.ModelSerializer): "sort_field", "sort_reverse", "filter_rules"] def update(self, instance, validated_data): - rules_data = validated_data.pop('filter_rules') + if 'filter_rules' in validated_data: + rules_data = validated_data.pop('filter_rules') + else: + rules_data = None super(SavedViewSerializer, self).update(instance, validated_data) - SavedViewFilterRule.objects.filter(saved_view=instance).delete() - for rule_data in rules_data: - SavedViewFilterRule.objects.create(saved_view=instance, **rule_data) + if rules_data: + SavedViewFilterRule.objects.filter(saved_view=instance).delete() + for rule_data in rules_data: + SavedViewFilterRule.objects.create(saved_view=instance, **rule_data) return instance def create(self, validated_data): From 999b36473c933c3029fb54c983d05babca9f39a6 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Tue, 15 Dec 2020 03:29:23 +0100 Subject: [PATCH 0118/1708] more refactoring and bug fixing. --- .../filter-editor/filter-editor.component.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/filter-editor/filter-editor.component.ts index f98b9517f..a11f0736a 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.ts @@ -200,24 +200,21 @@ export class FilterEditorComponent implements OnInit, OnDestroy { } setDateFilter(date: NgbDateStruct, dateRuleTypeID: number) { - let filterRules = this.filterRules - let existingRule = filterRules.find(rule => rule.rule_type == dateRuleTypeID) + let existingRule = this.filterRules.find(rule => rule.rule_type == dateRuleTypeID) let newValue = this.dateParser.format(date) if (existingRule) { existingRule.value = newValue } else { - filterRules.push({rule_type: dateRuleTypeID, value: newValue}) + this.filterRules.push({rule_type: dateRuleTypeID, value: newValue}) } - - this.filterRules = filterRules } clearDateFilter(dateRuleTypeID: number) { - let filterRules = this.filterRules - let existingRule = filterRules.find(rule => rule.rule_type == dateRuleTypeID) - filterRules.splice(filterRules.indexOf(existingRule), 1) - this.filterRules = filterRules + let ruleIndex = this.filterRules.findIndex(rule => rule.rule_type == dateRuleTypeID) + if (ruleIndex != -1) { + this.filterRules.splice(ruleIndex, 1) + } } } From 6d39dfeb3b096d994d303e8f89faeb09142a2eaa Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Tue, 15 Dec 2020 03:53:18 +0100 Subject: [PATCH 0119/1708] forgot to address this. --- .../components/document-list/document-list.component.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index acbfd3602..1b3596098 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -93,16 +93,16 @@ </td> <td class="d-none d-md-table-cell"> <ng-container *ngIf="d.correspondent"> - <a [routerLink]="" (click)="clickCorrespondent(d.correspondent)" title="Filter by correspondent">{{(d.correspondent$ | async)?.name}}</a> + <a [routerLink]="" (click)="clickCorrespondent(d.correspondent.id)" title="Filter by correspondent">{{(d.correspondent$ | async)?.name}}</a> </ng-container> </td> <td> <a routerLink="/documents/{{d.id}}" title="Edit document" style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a> - <app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ml-1" clickable="true" linkTitle="Filter by tag" (click)="clickTag(t)"></app-tag> + <app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ml-1" clickable="true" linkTitle="Filter by tag" (click)="clickTag(t.id)"></app-tag> </td> <td class="d-none d-xl-table-cell"> <ng-container *ngIf="d.document_type"> - <a [routerLink]="" (click)="clickDocumentType(d.document_type)" title="Filter by document type">{{(d.document_type$ | async)?.name}}</a> + <a [routerLink]="" (click)="clickDocumentType(d.document_type.id)" title="Filter by document type">{{(d.document_type$ | async)?.name}}</a> </ng-container> </td> <td> From 164755c755a170678bfd648f6f72261eee7c4af6 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Mon, 14 Dec 2020 19:45:22 -0800 Subject: [PATCH 0120/1708] Breakpoints for screen sizes, icons for mobile --- .../document-list.component.html | 2 +- .../filter-dropdown.component.html | 14 +++++- .../filter-dropdown.component.ts | 2 +- .../filter-editor.component.html | 45 ++++++++++--------- 4 files changed, 40 insertions(+), 23 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index acbfd3602..5f91deb5f 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -62,7 +62,7 @@ </app-page-header> -<div class="w-100 mb-4"> +<div class="w-100 mb-2 mb-sm-4"> <app-filter-editor [(filterRules)]="list.filterRules" #filterEditor></app-filter-editor> </div> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html index 5de9228bd..76ef360fd 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html @@ -1,6 +1,18 @@ <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'"> - {{title}} + <div class="d-none d-md-inline">{{title}}</div> + <div class="d-inline-block d-md-none" [ngSwitch]="icon"> + <svg *ngSwitchCase="'person-fill'" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person-fill" viewBox="0 0 16 16"> + <path fill-rule="evenodd" d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z" /> + </svg> + <svg *ngSwitchCase="'tag-fill'" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-tags-fill" viewBox="0 0 16 16"> + <path fill-rule="evenodd" d="M3 1a1 1 0 0 0-1 1v4.586a1 1 0 0 0 .293.707l7 7a1 1 0 0 0 1.414 0l4.586-4.586a1 1 0 0 0 0-1.414l-7-7A1 1 0 0 0 7.586 1H3zm4 3.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z" /> + <path d="M1 7.086a1 1 0 0 0 .293.707L8.75 15.25l-.043.043a1 1 0 0 1-1.414 0l-7-7A1 1 0 0 1 0 7.586V3a1 1 0 0 1 1-1v5.086z" /> + </svg> + <svg *ngSwitchCase="'file-earmark-fill'" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-fill" viewBox="0 0 16 16"> + <path fill-rule="evenodd" d="M4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.707A1 1 0 0 0 13.707 4L10 .293A1 1 0 0 0 9.293 0H4zm5.5 1.5v2a1 1 0 0 0 1 1h2l-3-3z" /> + </svg> + </div> </button> <div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> <div class="list-group list-group-flush"> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts index d675e14f1..b9d3fca6f 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.ts @@ -22,7 +22,7 @@ export class FilterDropdownComponent { title: string @Input() - display: string + icon: string @Output() toggle = new EventEmitter() diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.html b/src-ui/src/app/components/filter-editor/filter-editor.component.html index 80f10407c..5bf23f8bd 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.html +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.html @@ -1,22 +1,27 @@ -<div class="form-row form-group mb-0"> - <div class="col-auto"> - <div class="text-muted mt-1">Filter by:</div> +<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="col"> - <input class="form-control form-control-sm" type="text" [(ngModel)]="titleFilter" placeholder="Title"> - </div> - - <app-filter-dropdown class="col-auto" [items]="tags" [itemsSelected]="selectedTags" title="Tags" (toggle)="toggleTag($event.id)"></app-filter-dropdown> - <app-filter-dropdown class="col-auto" [items]="correspondents" [itemsSelected]="selectedCorrespondents" title="Correspondents" (toggle)="toggleCorrespondent($event.id)"></app-filter-dropdown> - <app-filter-dropdown class="col-auto" [items]="documentTypes" [itemsSelected]="selectedDocumentTypes" title="Document types" (toggle)="toggleDocumentType($event.id)"></app-filter-dropdown> - - <app-filter-dropdown-date class="col-auto" [dateBefore]="dateCreatedBefore" [dateAfter]="dateCreatedAfter" title="Created" (datesSet)="onDatesCreatedSet($event)"></app-filter-dropdown-date> - <app-filter-dropdown-date class="col-auto" [dateBefore]="dateAddedBefore" [dateAfter]="dateAddedAfter" title="Added" (datesSet)="onDatesAddedSet($event)"></app-filter-dropdown-date> - - <button class="btn btn-link btn-sm" [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 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" [items]="tags" [itemsSelected]="selectedTags" title="Tags" icon="tag-fill" (toggle)="toggleTag($event.id)"></app-filter-dropdown> + <app-filter-dropdown class="mr-2" [items]="correspondents" [itemsSelected]="selectedCorrespondents" title="Correspondents" icon="person-fill" (toggle)="toggleCorrespondent($event.id)"></app-filter-dropdown> + <app-filter-dropdown class="mr-2" [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" [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> From 30185d560ca363b53deed34c747b8a201ef58ab0 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Mon, 14 Dec 2020 22:33:50 -0800 Subject: [PATCH 0121/1708] Much cleaner way to set icon --- .../filter-dropdown/filter-dropdown.component.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html index 76ef360fd..8f7d14e81 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html @@ -11,6 +11,9 @@ </svg> <svg *ngSwitchCase="'file-earmark-fill'" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-fill" viewBox="0 0 16 16"> <path fill-rule="evenodd" d="M4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.707A1 1 0 0 0 13.707 4L10 .293A1 1 0 0 0 9.293 0H4zm5.5 1.5v2a1 1 0 0 0 1 1h2l-3-3z" /> + <div class="d-inline-block d-md-none"> + <svg class="toolbaricon" fill="currentColor"> + <use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" /> </svg> </div> </button> From 3b2bc292d80708332f32c903af9a01104ff8a34e Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Mon, 14 Dec 2020 23:14:04 -0800 Subject: [PATCH 0122/1708] Tweak checkbox --- .../document-card-small.component.html | 2 +- .../document-card-small.component.scss | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html index 4ced42bdd..378047602 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html @@ -3,7 +3,7 @@ <div class="border-bottom" [class.doc-img-background-selected]="selected"> <img class="card-img doc-img" [src]="getThumbUrl()" (click)="selected = !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"> <input type="checkbox" class="custom-control-input" id="smallCardCheck{{document.id}}" [checked]="selected" (change)="selected = $event.target.checked"> <label class="custom-control-label" for="smallCardCheck{{document.id}}"></label> diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.scss b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.scss index 36db2203c..a4af1bb11 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.scss +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.scss @@ -8,7 +8,15 @@ } .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 { @@ -17,8 +25,12 @@ .card-selected { border-color: $primary; + + .document-card-check { + display: block; + } } .doc-img-background-selected { background-color: $primaryFaded; -} \ No newline at end of file +} From b45bd665736879afcc776d876a5eacad35000b66 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Mon, 14 Dec 2020 23:14:19 -0800 Subject: [PATCH 0123/1708] Basic bulk editor component --- src-ui/src/app/app.module.ts | 2 + .../bulk-editor/bulk-editor.component.html | 16 +++++++ .../bulk-editor/bulk-editor.component.scss | 0 .../bulk-editor/bulk-editor.component.spec.ts | 25 ++++++++++ .../bulk-editor/bulk-editor.component.ts | 46 +++++++++++++++++++ .../document-list.component.html | 30 +++++++----- .../document-list/document-list.component.ts | 4 ++ 7 files changed, 111 insertions(+), 12 deletions(-) create mode 100644 src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html create mode 100644 src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.scss create mode 100644 src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts create mode 100644 src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index 914854892..627d4f6cf 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -32,6 +32,7 @@ import { FilterDropdownButtonComponent } from './components/filter-editor/filter import { FilterDropdownDateComponent } from './components/filter-editor/filter-dropdown-date/filter-dropdown-date.component'; import { DocumentCardLargeComponent } from './components/document-list/document-card-large/document-card-large.component'; import { DocumentCardSmallComponent } from './components/document-list/document-card-small/document-card-small.component'; +import { BulkEditorComponent } from './components/document-list/bulk-editor/bulk-editor.component'; import { NgxFileDropModule } from 'ngx-file-drop'; import { TextComponent } from './components/common/input/text/text.component'; import { SelectComponent } from './components/common/input/select/select.component'; @@ -84,6 +85,7 @@ import { SelectDialogComponent } from './components/common/select-dialog/select- FilterDropdownDateComponent, DocumentCardLargeComponent, DocumentCardSmallComponent, + BulkEditorComponent, TextComponent, SelectComponent, CheckComponent, diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html new file mode 100644 index 000000000..a1574f6f7 --- /dev/null +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html @@ -0,0 +1,16 @@ +<div class="btn-group mr-lg-2" role="group" aria-label="Select"> + <button class="btn btn-sm btn-outline-primary" (click)="this.selectPage.next()">Select page</button> + <button class="btn btn-sm btn-outline-primary" (click)="this.selectAll.next()">Select all</button> + <button class="btn btn-sm btn-outline-primary" (click)="this.selectNone.next()">Select none</button> +</div> +<div class="btn-group mr-lg-2" role="group" aria-label="Actions"> + <button class="btn btn-sm btn-outline-primary" (click)="this.setCorrespondent.next()">Set correspondent</button> + <button class="btn btn-sm btn-outline-primary" (click)="this.removeCorrespondent.next()">Remove correspondent</button> + <button class="btn btn-sm btn-outline-primary" (click)="this.setDocumentType.next()">Set document type</button> + <button class="btn btn-sm btn-outline-primary" (click)="this.removeDocumentType.next()">Remove document type</button> + <button class="btn btn-sm btn-outline-primary" (click)="this.addTag.next()">Add tag</button> + <button class="btn btn-sm btn-outline-primary" (click)="this.removeTag.next()">Remove tag</button> +</div> +<div class="btn-group mr-lg-2" role="group" aria-label="Delete"> + <button class="btn btn-sm btn-outline-primary" (click)="this.delete.next()">Delete</button> +</div> diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.scss b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts new file mode 100644 index 000000000..140d73301 --- /dev/null +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts @@ -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(); + }); +}); diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts new file mode 100644 index 000000000..7459d62dc --- /dev/null +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -0,0 +1,46 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { DocumentListViewService } from 'src/app/services/document-list-view.service'; + +@Component({ + selector: 'app-bulk-editor', + templateUrl: './bulk-editor.component.html', + styleUrls: ['./bulk-editor.component.scss'] +}) +export class BulkEditorComponent { + + @Input() + list: DocumentListViewService + + @Output() + selectPage = new EventEmitter() + + @Output() + selectAll = new EventEmitter() + + @Output() + selectNone = new EventEmitter() + + @Output() + setCorrespondent = new EventEmitter() + + @Output() + removeCorresponden = new EventEmitter() + + @Output() + setDocumentType = new EventEmitter() + + @Output() + removeDocumentType = new EventEmitter() + + @Output() + addTag = new EventEmitter() + + @Output() + removeTag = new EventEmitter() + + @Output() + delete = new EventEmitter() + + constructor( ) { } + +} diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index be2ed4847..36e9ff8fd 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -1,25 +1,16 @@ <app-page-header [title]="getTitle()"> <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"> <use xlink:href="assets/bootstrap-icons.svg#text-indent-left" /> </svg> - Bulk edit + Select </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.selectAll()">Select all</button> <button ngbDropdownItem (click)="list.selectNone()">Select none</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> @@ -96,6 +87,21 @@ [rotate]="true" (pageChange)="list.reload()" aria-label="Default pagination"></ngb-pagination> </div> +<div class="w-100 mb-2" [ngbCollapse]="!isBulkEditing"> + <app-bulk-editor + (selectPage)="list.selectPage()" + (selectAll)="list.selectAll()" + (selectNone)="list.selectNone()" + (setCorrespondent)="bulkSetCorrespondent()" + (removeCorrespondent)="bulkRemoveCorrespondent()" + (setDocumentType)="bulkSetDocumentType()" + (removeDocumentType)="bulkRemoveDocumentType()" + (addTag)="bulkAddTag()" + (removeTag)="bulkRemoveTag()" + (delete)="bulkDelete()"> + </app-bulk-editor> +</div> + <div *ngIf="displayMode == 'largeCards'"> <app-document-card-large *ngFor="let d of list.documents" [document]="d" [details]="d.content" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)"> </app-document-card-large> diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index 1bd1e5c7f..0a6fa4352 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -52,6 +52,10 @@ export class DocumentListComponent implements OnInit { return DOCUMENT_SORT_FIELDS } + get isBulkEditing(): boolean { + return this.list.selected.size > 0 + } + saveDisplayMode() { localStorage.setItem('document-list:displayMode', this.displayMode) } From 34c42c4339a9ef8b7f5d03d76be1eddb6a84d865 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Mon, 14 Dec 2020 23:39:10 -0800 Subject: [PATCH 0124/1708] Better svgs --- .../filter-dropdown-button.component.html | 4 ++-- .../app/components/filter-editor/filter-editor.component.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.html index 8dff12a33..0ea870533 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component.html @@ -1,7 +1,7 @@ <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 *ngIf="selected" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> + <use xlink:href="assets/bootstrap-icons.svg#check" /> </svg> </div> <div class="mr-1"> diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.html b/src-ui/src/app/components/filter-editor/filter-editor.component.html index 80f10407c..e07edba14 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.html +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.html @@ -14,8 +14,8 @@ <app-filter-dropdown-date class="col-auto" [dateBefore]="dateAddedBefore" [dateAfter]="dateAddedAfter" title="Added" (datesSet)="onDatesAddedSet($event)"></app-filter-dropdown-date> <button class="btn btn-link btn-sm" [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 width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> + <use xlink:href="assets/bootstrap-icons.svg#x" /> </svg> Clear all filters </button> From 03f071fd27c2e7e003655d534a8affe28d48b871 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Tue, 15 Dec 2020 00:57:31 -0800 Subject: [PATCH 0125/1708] Styled, organized button UI --- .../bulk-editor/bulk-editor.component.html | 101 +++++++++++++++--- .../bulk-editor/bulk-editor.component.scss | 6 ++ .../bulk-editor/bulk-editor.component.ts | 2 +- .../document-list.component.html | 2 +- 4 files changed, 95 insertions(+), 16 deletions(-) diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html index a1574f6f7..54212923a 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html @@ -1,16 +1,89 @@ -<div class="btn-group mr-lg-2" role="group" aria-label="Select"> - <button class="btn btn-sm btn-outline-primary" (click)="this.selectPage.next()">Select page</button> - <button class="btn btn-sm btn-outline-primary" (click)="this.selectAll.next()">Select all</button> - <button class="btn btn-sm btn-outline-primary" (click)="this.selectNone.next()">Select none</button> +<div class="card bg-light"> +<div class="card-body px-2 py-2 d-flex justify-content-between flex-wrap align-items-end"> + <div class="d-flex flex-grow-1 mb-2 mb-lg-0" role="group" aria-label="Select"> + <label class="d-flex mt-1 mr-auto mr-lg-2">Select:</label> + <div class="btn-group d-flex"> + <button class="btn btn-sm btn-outline-primary" (click)="selectPage.next()"> + <svg viewBox="0 0 16 16" fill="currentColor"> + <use xlink:href="assets/bootstrap-icons.svg#file-earmark-check" /> + </svg> + Page + </button> + <button class="btn btn-sm btn-outline-primary" (click)="selectAll.next()"> + <svg viewBox="0 0 16 16" fill="currentColor"> + <use xlink:href="assets/bootstrap-icons.svg#check-all" /> + </svg> + All + </button> + <button class="btn btn-sm btn-outline-primary" (click)="selectNone.next()"> + <svg viewBox="0 0 16 16" fill="currentColor"> + <use xlink:href="assets/bootstrap-icons.svg#slash-circle" /> + </svg> + None + </button> + </div> + </div> + <div class="d-flex flex-grow-1 mb-2 mb-lg-0" role="group" aria-label="Tags"> + <label class="d-flex mt-1 mr-auto mr-lg-2">Tags:</label> + <div class="btn-group d-flex"> + <button class="btn btn-sm btn-outline-primary" (click)="addTag.next()"> + <ng-container *ngTemplateOutlet="add"></ng-container> + </button> + <button class="btn btn-sm btn-outline-primary" (click)="removeTag.next()"> + <ng-container *ngTemplateOutlet="remove"></ng-container> + </button> + </div> + </div> + <div class="d-flex flex-grow-1 mb-2 mb-lg-0" role="group" aria-label="Correspondent"> + <label class="d-flex mt-1 mr-auto mr-lg-2">Correspondent:</label> + <div class="btn-group d-flex"> + <button class="btn btn-sm btn-outline-primary" (click)="setCorrespondent.next()"> + <ng-container *ngTemplateOutlet="edit"></ng-container> + </button> + <button class="btn btn-sm btn-outline-primary" (click)="removeCorrespondent.next()"> + <ng-container *ngTemplateOutlet="remove"></ng-container> + </button> + </div> + </div> + <div class="d-flex flex-grow-1 mb-2 mb-lg-0" role="group" aria-label="Document Type"> + <label class="d-flex mt-1 mr-auto mr-lg-2">Document Type:</label> + <div class="btn-group d-flex"> + <button class="btn btn-sm btn-outline-primary" (click)="setDocumentType.next()"> + <ng-container *ngTemplateOutlet="edit"></ng-container> + </button> + <button class="btn btn-sm btn-outline-primary" (click)="removeDocumentType.next()"> + <ng-container *ngTemplateOutlet="remove"></ng-container> + </button> + </div> + </div> + <div class="d-flex flex-grow-1 mb-2 mb-lg-0" role="group" aria-label="Delete"> + <button class="btn btn-sm btn-outline-danger ml-auto" (click)="delete.next()"> + <svg viewBox="0 0 16 16" fill="currentColor"> + <use xlink:href="assets/bootstrap-icons.svg#trash" /> + </svg> + Delete + </button> + </div> </div> -<div class="btn-group mr-lg-2" role="group" aria-label="Actions"> - <button class="btn btn-sm btn-outline-primary" (click)="this.setCorrespondent.next()">Set correspondent</button> - <button class="btn btn-sm btn-outline-primary" (click)="this.removeCorrespondent.next()">Remove correspondent</button> - <button class="btn btn-sm btn-outline-primary" (click)="this.setDocumentType.next()">Set document type</button> - <button class="btn btn-sm btn-outline-primary" (click)="this.removeDocumentType.next()">Remove document type</button> - <button class="btn btn-sm btn-outline-primary" (click)="this.addTag.next()">Add tag</button> - <button class="btn btn-sm btn-outline-primary" (click)="this.removeTag.next()">Remove tag</button> -</div> -<div class="btn-group mr-lg-2" role="group" aria-label="Delete"> - <button class="btn btn-sm btn-outline-primary" (click)="this.delete.next()">Delete</button> </div> + +<ng-template #add> + <svg viewBox="0 0 16 16" fill="currentColor"> + <use xlink:href="assets/bootstrap-icons.svg#plus-circle" /> + </svg> + Add +</ng-template> + +<ng-template #edit> + <svg viewBox="0 0 16 16" fill="currentColor"> + <use xlink:href="assets/bootstrap-icons.svg#pencil" /> + </svg> + Edit +</ng-template> + +<ng-template #remove> + <svg viewBox="0 0 16 16" fill="currentColor"> + <use xlink:href="assets/bootstrap-icons.svg#x-circle" /> + </svg> + Remove +</ng-template> diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.scss b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.scss index e69de29bb..3868e7a02 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.scss +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.scss @@ -0,0 +1,6 @@ +.btn svg { + width: 0.9em; + height: 0.9em; + margin-right: 2px; + margin-top: -1px; +} diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts index 7459d62dc..5c1ad01ae 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -24,7 +24,7 @@ export class BulkEditorComponent { setCorrespondent = new EventEmitter() @Output() - removeCorresponden = new EventEmitter() + removeCorrespondent = new EventEmitter() @Output() setDocumentType = new EventEmitter() diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index 36e9ff8fd..a88ad65b7 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -87,7 +87,7 @@ [rotate]="true" (pageChange)="list.reload()" aria-label="Default pagination"></ngb-pagination> </div> -<div class="w-100 mb-2" [ngbCollapse]="!isBulkEditing"> +<div class="w-100 mb-3" [ngbCollapse]="!isBulkEditing"> <app-bulk-editor (selectPage)="list.selectPage()" (selectAll)="list.selectAll()" From 49be87fe371d1f8a5895f42513a943b5bf2a5a7c Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Tue, 15 Dec 2020 12:06:24 +0100 Subject: [PATCH 0126/1708] code style --- src/documents/models.py | 6 +++++- src/documents/serialisers.py | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/documents/models.py b/src/documents/models.py index 1b1f697bc..b544f413d 100755 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -340,7 +340,11 @@ class SavedViewFilterRule(models.Model): (17, "Does not have tag"), ] - saved_view = models.ForeignKey(SavedView, on_delete=models.CASCADE, related_name="filter_rules") + saved_view = models.ForeignKey( + SavedView, + on_delete=models.CASCADE, + related_name="filter_rules" + ) rule_type = models.PositiveIntegerField(choices=RULE_TYPES) diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 2def07fdd..36878448c 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -167,14 +167,16 @@ class SavedViewSerializer(serializers.ModelSerializer): if rules_data: SavedViewFilterRule.objects.filter(saved_view=instance).delete() for rule_data in rules_data: - SavedViewFilterRule.objects.create(saved_view=instance, **rule_data) + SavedViewFilterRule.objects.create( + saved_view=instance, **rule_data) return instance def create(self, validated_data): rules_data = validated_data.pop('filter_rules') saved_view = SavedView.objects.create(**validated_data) for rule_data in rules_data: - SavedViewFilterRule.objects.create(saved_view=saved_view, **rule_data) + SavedViewFilterRule.objects.create( + saved_view=saved_view, **rule_data) return saved_view From 56204933b0c045de0a4b5156c781269cfc40c3c1 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Tue, 15 Dec 2020 13:16:28 +0100 Subject: [PATCH 0127/1708] bugfix, tests --- src/documents/serialisers.py | 2 +- src/documents/tests/test_api.py | 91 +++++++++++++++++++++++++++++-- src/documents/tests/test_index.py | 21 +++++++ 3 files changed, 109 insertions(+), 5 deletions(-) diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 36878448c..ee0a42384 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -164,7 +164,7 @@ class SavedViewSerializer(serializers.ModelSerializer): else: rules_data = None super(SavedViewSerializer, self).update(instance, validated_data) - if rules_data: + if rules_data is not None: SavedViewFilterRule.objects.filter(saved_view=instance).delete() for rule_data in rules_data: SavedViewFilterRule.objects.create( diff --git a/src/documents/tests/test_api.py b/src/documents/tests/test_api.py index ab1716366..e0a64664f 100644 --- a/src/documents/tests/test_api.py +++ b/src/documents/tests/test_api.py @@ -4,12 +4,11 @@ import tempfile from unittest import mock from django.contrib.auth.models import User -from pathvalidate import ValidationError from rest_framework.test import APITestCase from whoosh.writing import AsyncWriter from documents import index -from documents.models import Document, Correspondent, DocumentType, Tag +from documents.models import Document, Correspondent, DocumentType, Tag, SavedView from documents.tests.utils import DirectoriesMixin @@ -18,8 +17,8 @@ class TestDocumentApi(DirectoriesMixin, APITestCase): def setUp(self): super(TestDocumentApi, self).setUp() - user = User.objects.create_superuser(username="temp_admin") - self.client.force_login(user=user) + self.user = User.objects.create_superuser(username="temp_admin") + self.client.force_login(user=self.user) def testDocuments(self): @@ -515,3 +514,87 @@ class TestDocumentApi(DirectoriesMixin, APITestCase): self.assertFalse(meta['has_archive_version']) self.assertGreater(len(meta['original_metadata']), 0) self.assertIsNone(meta['archive_metadata']) + + def test_saved_views(self): + u1 = User.objects.create_user("user1") + u2 = User.objects.create_user("user2") + + v1 = SavedView.objects.create(user=u1, name="test1", sort_field="", show_on_dashboard=False, show_in_sidebar=False) + v2 = SavedView.objects.create(user=u2, name="test2", sort_field="", show_on_dashboard=False, show_in_sidebar=False) + v3 = SavedView.objects.create(user=u2, name="test3", sort_field="", show_on_dashboard=False, show_in_sidebar=False) + + response = self.client.get("/api/saved_views/") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['count'], 0) + + self.assertEqual(self.client.get(f"/api/saved_views/{v1.id}/").status_code, 404) + + self.client.force_login(user=u1) + + response = self.client.get("/api/saved_views/") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['count'], 1) + + self.assertEqual(self.client.get(f"/api/saved_views/{v1.id}/").status_code, 200) + + self.client.force_login(user=u2) + + response = self.client.get("/api/saved_views/") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['count'], 2) + + self.assertEqual(self.client.get(f"/api/saved_views/{v1.id}/").status_code, 404) + + def test_create_update_patch(self): + + u1 = User.objects.create_user("user1") + + view = { + "name": "test", + "show_on_dashboard": True, + "show_in_sidebar": True, + "sort_field": "created2", + "filter_rules": [ + { + "rule_type": 4, + "value": "test" + } + ] + } + + response = self.client.post("/api/saved_views/", view, format='json') + self.assertEqual(response.status_code, 201) + + v1 = SavedView.objects.get(name="test") + self.assertEqual(v1.sort_field, "created2") + self.assertEqual(v1.filter_rules.count(), 1) + self.assertEqual(v1.user, self.user) + + response = self.client.patch(f"/api/saved_views/{v1.id}/", { + "show_in_sidebar": False + }, format='json') + + v1 = SavedView.objects.get(id=v1.id) + self.assertEqual(response.status_code, 200) + self.assertFalse(v1.show_in_sidebar) + self.assertEqual(v1.filter_rules.count(), 1) + + view['filter_rules'] = [{ + "rule_type": 12, + "value": "secret" + }] + + response = self.client.put(f"/api/saved_views/{v1.id}/", view, format='json') + self.assertEqual(response.status_code, 200) + + v1 = SavedView.objects.get(id=v1.id) + self.assertEqual(v1.filter_rules.count(), 1) + self.assertEqual(v1.filter_rules.first().value, "secret") + + view['filter_rules'] = [] + + response = self.client.put(f"/api/saved_views/{v1.id}/", view, format='json') + self.assertEqual(response.status_code, 200) + + v1 = SavedView.objects.get(id=v1.id) + self.assertEqual(v1.filter_rules.count(), 0) diff --git a/src/documents/tests/test_index.py b/src/documents/tests/test_index.py index 830fca0e0..2baa9621d 100644 --- a/src/documents/tests/test_index.py +++ b/src/documents/tests/test_index.py @@ -1,6 +1,9 @@ from django.test import TestCase +from documents import index from documents.index import JsonFormatter +from documents.models import Document +from documents.tests.utils import DirectoriesMixin class JsonFormatterTest(TestCase): @@ -12,3 +15,21 @@ class JsonFormatterTest(TestCase): self.assertListEqual(self.formatter.format([]), []) +class TestAutoComplete(DirectoriesMixin, TestCase): + + def test_auto_complete(self): + + doc1 = Document.objects.create(title="doc1", checksum="A", content="test test2 test3") + doc2 = Document.objects.create(title="doc2", checksum="B", content="test test2") + doc3 = Document.objects.create(title="doc3", checksum="C", content="test2") + + index.add_or_update_document(doc1) + index.add_or_update_document(doc2) + index.add_or_update_document(doc3) + + ix = index.open_index() + + self.assertListEqual(index.autocomplete(ix, "tes"), [b"test3", b"test", b"test2"]) + self.assertListEqual(index.autocomplete(ix, "tes", limit=3), [b"test3", b"test", b"test2"]) + self.assertListEqual(index.autocomplete(ix, "tes", limit=1), [b"test3"]) + self.assertListEqual(index.autocomplete(ix, "tes", limit=0), []) From 7e0aa7136aed44d7cf200c5a816a372c282acc4a Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Tue, 15 Dec 2020 13:26:01 +0100 Subject: [PATCH 0128/1708] more tests --- src/paperless_text/tests/samples/test.txt | 1 + src/paperless_text/tests/test_parser.py | 26 +++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 src/paperless_text/tests/samples/test.txt create mode 100644 src/paperless_text/tests/test_parser.py diff --git a/src/paperless_text/tests/samples/test.txt b/src/paperless_text/tests/samples/test.txt new file mode 100644 index 000000000..6de7b8c69 --- /dev/null +++ b/src/paperless_text/tests/samples/test.txt @@ -0,0 +1 @@ +This is a test file. diff --git a/src/paperless_text/tests/test_parser.py b/src/paperless_text/tests/test_parser.py new file mode 100644 index 000000000..413aa91cf --- /dev/null +++ b/src/paperless_text/tests/test_parser.py @@ -0,0 +1,26 @@ +import os + +from django.test import TestCase + +from documents.tests.utils import DirectoriesMixin +from paperless_text.parsers import TextDocumentParser + + +class TestTextParser(DirectoriesMixin, TestCase): + + def test_thumbnail(self): + + parser = TextDocumentParser(None) + + # just make sure that it does not crash + f = parser.get_thumbnail(os.path.join(os.path.dirname(__file__), "samples", "test.txt"), "text/plain") + self.assertTrue(os.path.isfile(f)) + + def test_parse(self): + + parser = TextDocumentParser(None) + + parser.parse(os.path.join(os.path.dirname(__file__), "samples", "test.txt"), "text/plain") + + self.assertEqual(parser.get_text(), "This is a test file.\n") + self.assertIsNone(parser.get_archive_path()) From b787983e42434daadae4ff0c309329f91efc8781 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Tue, 15 Dec 2020 13:47:43 +0100 Subject: [PATCH 0129/1708] more tests --- src/documents/models.py | 2 +- src/documents/tests/test_sanity_check.py | 10 +++++++--- src/documents/tests/utils.py | 3 ++- src/paperless_mail/mail.py | 10 +++++----- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/documents/models.py b/src/documents/models.py index b544f413d..d81343afa 100755 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -221,7 +221,7 @@ class Document(models.Model): else: fname = "{:07}{}".format(self.pk, self.file_type) if self.storage_type == self.STORAGE_TYPE_GPG: - fname += ".gpg" + fname += ".gpg" # pragma: no cover return os.path.join( settings.ORIGINALS_DIR, diff --git a/src/documents/tests/test_sanity_check.py b/src/documents/tests/test_sanity_check.py index 725e87617..0554cd7cd 100644 --- a/src/documents/tests/test_sanity_check.py +++ b/src/documents/tests/test_sanity_check.py @@ -2,6 +2,8 @@ import os import shutil from pathlib import Path +import filelock +from django.conf import settings from django.test import TestCase from documents.models import Document @@ -13,9 +15,11 @@ class TestSanityCheck(DirectoriesMixin, TestCase): def make_test_data(self): - shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "documents", "originals", "0000001.pdf"), os.path.join(self.dirs.originals_dir, "0000001.pdf")) - shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "documents", "archive", "0000001.pdf"), os.path.join(self.dirs.archive_dir, "0000001.pdf")) - shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "documents", "thumbnails", "0000001.png"), os.path.join(self.dirs.thumbnail_dir, "0000001.png")) + with filelock.FileLock(settings.MEDIA_LOCK): + # just make sure that the lockfile is present. + shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "documents", "originals", "0000001.pdf"), os.path.join(self.dirs.originals_dir, "0000001.pdf")) + shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "documents", "archive", "0000001.pdf"), os.path.join(self.dirs.archive_dir, "0000001.pdf")) + shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "documents", "thumbnails", "0000001.png"), os.path.join(self.dirs.thumbnail_dir, "0000001.png")) return Document.objects.create(title="test", checksum="42995833e01aea9b3edee44bbfdd7ce1", archive_checksum="62acb0bcbfbcaa62ca6ad3668e4e404b", content="test", pk=1, filename="0000001.pdf", mime_type="application/pdf") diff --git a/src/documents/tests/utils.py b/src/documents/tests/utils.py index 7f9d50ed5..dfefc4061 100644 --- a/src/documents/tests/utils.py +++ b/src/documents/tests/utils.py @@ -34,7 +34,8 @@ def setup_directories(): ARCHIVE_DIR=dirs.archive_dir, CONSUMPTION_DIR=dirs.consumption_dir, INDEX_DIR=dirs.index_dir, - MODEL_FILE=os.path.join(dirs.data_dir, "classification_model.pickle") + MODEL_FILE=os.path.join(dirs.data_dir, "classification_model.pickle"), + MEDIA_LOCK=os.path.join(dirs.media_dir, "media.lock") ) dirs.settings_override.enable() diff --git a/src/paperless_mail/mail.py b/src/paperless_mail/mail.py index a82c34f15..3c200362d 100644 --- a/src/paperless_mail/mail.py +++ b/src/paperless_mail/mail.py @@ -26,7 +26,7 @@ class BaseMailAction: return {} def post_consume(self, M, message_uids, parameter): - pass + pass # pragma: nocover class DeleteMailAction(BaseMailAction): @@ -69,7 +69,7 @@ def get_rule_action(rule): elif rule.action == MailRule.ACTION_MARK_READ: return MarkReadMailAction() else: - raise ValueError("Unknown action.") + raise NotImplementedError("Unknown action.") # pragma: nocover def make_criterias(rule): @@ -95,7 +95,7 @@ def get_mailbox(server, port, security): elif security == MailAccount.IMAP_SECURITY_SSL: mailbox = MailBox(server, port) else: - raise ValueError("Unknown IMAP security") + raise NotImplementedError("Unknown IMAP security") # pragma: nocover return mailbox @@ -119,7 +119,7 @@ class MailAccountHandler(LoggingMixin): return os.path.splitext(os.path.basename(att.filename))[0] else: - raise ValueError("Unknown title selector.") + raise NotImplementedError("Unknown title selector.") # pragma: nocover # NOQA: E501 def get_correspondent(self, message, rule): c_from = rule.assign_correspondent_from @@ -141,7 +141,7 @@ class MailAccountHandler(LoggingMixin): return rule.assign_correspondent else: - raise ValueError("Unknwown correspondent selector") + raise NotImplementedError("Unknwown correspondent selector") # pragma: nocover # NOQA: E501 def handle_mail_account(self, account): From 5894060dc5e63b9109427816d76b94d66a132de5 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Tue, 15 Dec 2020 13:52:35 +0100 Subject: [PATCH 0130/1708] fixes #25 --- src/documents/parsers.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/documents/parsers.py b/src/documents/parsers.py index 228e2c86e..cbbb912de 100644 --- a/src/documents/parsers.py +++ b/src/documents/parsers.py @@ -163,8 +163,6 @@ def parse_date(filename, text): date = None - next_year = timezone.now().year + 5 # Arbitrary 5 year future limit - # if filename date parsing is enabled, search there first: if settings.FILENAME_DATE_ORDER: for m in re.finditer(DATE_REGEX, filename): @@ -176,7 +174,7 @@ def parse_date(filename, text): # Skip all matches that do not parse to a proper date continue - if date is not None and next_year > date.year > 1900: + if date and date.year > 1900 and date <= timezone.now(): return date # Iterate through all regex matches in text and try to parse the date @@ -189,7 +187,7 @@ def parse_date(filename, text): # Skip all matches that do not parse to a proper date continue - if date is not None and next_year > date.year > 1900: + if date and date.year > 1900 and date <= timezone.now(): break else: date = None From 31bea6a361e24d7ac2e42e70624edf08caaffc3c Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Tue, 15 Dec 2020 14:11:05 +0100 Subject: [PATCH 0131/1708] path sanitation --- src/documents/file_handling.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/documents/file_handling.py b/src/documents/file_handling.py index c49493991..5643756ac 100644 --- a/src/documents/file_handling.py +++ b/src/documents/file_handling.py @@ -99,6 +99,11 @@ def generate_filename(doc, counter=0): tags = defaultdictNoStr(lambda: slugify(None), many_to_dictionary(doc.tags)) + tag_list = pathvalidate.sanitize_filename( + ",".join([tag.name for tag in doc.tags.all()]), + replacement_text="-" + ) + if doc.correspondent: correspondent = pathvalidate.sanitize_filename( doc.correspondent.name, replacement_text="-" @@ -127,7 +132,7 @@ def generate_filename(doc, counter=0): added_month=f"{doc.added.month:02}" if doc.added else "none", added_day=f"{doc.added.day:02}" if doc.added else "none", tags=tags, - tag_list=",".join([tag.name for tag in doc.tags.all()]) + tag_list=tag_list ).strip() path = path.strip(os.sep) From a0c74025e3c1cda4b17c044c13f7251f6a7fe96c Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Tue, 15 Dec 2020 14:30:31 +0100 Subject: [PATCH 0132/1708] changelog and docs --- docs/advanced_usage.rst | 36 +++++++++++++++++++++--------------- docs/changelog.rst | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/docs/advanced_usage.rst b/docs/advanced_usage.rst index b5ae254b3..48a86384c 100644 --- a/docs/advanced_usage.rst +++ b/docs/advanced_usage.rst @@ -263,10 +263,10 @@ using the identifier which it has assigned to each document. You will end up get files like ``0000123.pdf`` in your media directory. This isn't necessarily a bad thing, because you normally don't have to access these files manually. However, if you wish to name your files differently, you can do that by adjusting the -``PAPERLESS_FILENAME_FORMAT`` settings variable. +``PAPERLESS_FILENAME_FORMAT`` configuration option. -This variable allows you to configure the filename (folders are allowed!) using -placeholders. For example, setting +This variable allows you to configure the filename (folders are allowed) using +placeholders. For example, configuring this to .. code:: bash @@ -277,17 +277,16 @@ will create a directory structure as follows: .. code:: 2019/ - my_bank/ - statement-january-0000001.pdf - statement-february-0000002.pdf + My bank/ + Statement January.pdf + Statement February.pdf 2020/ - my_bank/ - statement-january-0000003.pdf - shoe_store/ - my_new_shoes-0000004.pdf - -Paperless appends the unique identifier of each document to the filename. This -avoids filename clashes. + My bank/ + Statement January.pdf + Letter.pdf + Letter_01.pdf + Shoe store/ + My new shoes.pdf .. danger:: @@ -299,6 +298,7 @@ Paperless provides the following placeholders withing filenames: * ``{correspondent}``: The name of the correspondent, or "none". * ``{document_type}``: The name of the document type, or "none". +* ``{tag_list}``: A comma separated list of all tags assigned to the document. * ``{title}``: The title of the document. * ``{created}``: The full date and time the document was created. * ``{created_year}``: Year created only. @@ -309,8 +309,14 @@ Paperless provides the following placeholders withing filenames: * ``{added_month}``: Month added only (number 1-12). * ``{added_day}``: Day added only (number 1-31). -Paperless will convert all values for the placeholders into values which are safe -for use in filenames. + +Paperless will try to conserve the information from your database as much as possible. +However, some characters that you can use in document titles and correspondent names (such +as ``: \ /`` and a couple more) are not allowed in filenames and will be replaced with dashes. + +If paperless detects that two documents share the same filename, paperless will automatically +append ``_01``, ``_02``, etc to the filename. This happens if all the placeholders in a filename +evaluate to the same value. .. hint:: diff --git a/docs/changelog.rst b/docs/changelog.rst index a50fc31d5..0e55cb144 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,40 @@ Changelog ********* + +paperless-ng 0.9.7 +################## + + +* Front end + + * Thanks to the hard work of `Michael Shamoon`_, paperless now comes with a much more streamlined UI for + filtering documents. + + * `Michael Shamoon`_ replaced the document preview with another component. This should fix compatibility with Safari browsers. + + * Paperless now stores your saved views on the server and associates them with your user account. You + will have to recreate your views. + +* Other additions and changes + + * The GitHub and documentation links now open in new tabs/windows. Thanks to `rYR79435`_. + * The new filename format field ``{tag_list}`` inserts a list of tags into the filename, separated by comma. + * The ``document_retagger`` no longer removes inbox tags or tags without matching rules. + * The new configuration option ``PAPERLESS_COOKIE_PREFIX`` allows you to run multiple instances of paperless on different ports. + This option enables you to be logged in into multiple instances by specifying different cookie names for each instance. + +* Fixes + + * Sometimes paperless would assign dates in the future to newly consumed documents. + * The filename format fields ``{created_month}`` and ``{created_day}`` now use a leading zero for single digit values. + * The filename format field ``{tags}`` can no longer be used without arguments. + * Paperless was not able to consume many images (especially images from mobile scanners) due to missing DPI information. + Paperless now assumes A4 paper size for PDF generation if no DPI information is present. + * Documents with empty titles could not be opened from the table view due to the link being empty. + * Fixed an issue with filenames containing special characters such as ``:`` not being accepted for upload. + + paperless-ng 0.9.6 ################## @@ -841,6 +875,8 @@ bulk of the work on this big change. * Initial release +.. _rYR79435: https://github.com/rYR79435 +.. _Michael Shamoon: https://github.com/shamoon .. _jayme-github: http://github.com/jayme-github .. _Brian Conn: https://github.com/TheConnMan .. _Christopher Luu: https://github.com/nuudles From 7dce57b9f76a5684f321d175a921fb6a8da0442f Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Tue, 15 Dec 2020 14:34:48 +0100 Subject: [PATCH 0133/1708] changelog --- docs/changelog.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0e55cb144..84d04bc7a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -17,8 +17,9 @@ paperless-ng 0.9.7 * `Michael Shamoon`_ replaced the document preview with another component. This should fix compatibility with Safari browsers. - * Paperless now stores your saved views on the server and associates them with your user account. You - will have to recreate your views. + * Paperless now stores your saved views on the server and associates them with your user account. + This means that you can access your views on multiple devices and have separate views for different users. + You will have to recreate your views. * Other additions and changes From 55075619c10fd1dc3f2e607c4e6e418aa95a1146 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Tue, 15 Dec 2020 14:43:07 +0100 Subject: [PATCH 0134/1708] documentation --- docs/usage_overview.rst | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/usage_overview.rst b/docs/usage_overview.rst index bb9ecd452..d6f4cf9db 100644 --- a/docs/usage_overview.rst +++ b/docs/usage_overview.rst @@ -57,9 +57,6 @@ Adding documents to paperless ############################# Once you've got Paperless setup, you need to start feeding documents into it. -Currently, there are four options: the consumption directory, the dashboard, IMAP (email), and -HTTP POST. - When adding documents to paperless, it will perform the following operations on your documents: @@ -112,6 +109,17 @@ Dashboard upload The dashboard has a file drop field to upload documents to paperless. Simply drag a file onto this field or select a file with the file dialog. Multiple files are supported. + +Mobile upload +============= + +The mobile app over at `<https://github.com/qcasey/paperless_share>`_ allows Android users +to share any documents with paperless. This can be combined with any of the mobile +scanning apps out there, such as Office Lens. + +The app is still a little rough around the edges, +but it gets the job done. This will eventually be rolled into `Paperless App <https://github.com/bauerj/paperless_app>`_ as well. + .. _usage-email: IMAP (Email) From d208ab1e1299c30a9c98cad98c8810a3f18c6314 Mon Sep 17 00:00:00 2001 From: Jonas Winkler <dev@jpwinkler.de> Date: Tue, 15 Dec 2020 15:03:00 +0100 Subject: [PATCH 0135/1708] Update README.md --- README.md | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 41f85af19..276521f5b 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [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-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, see below. +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. This project is still in development and some things may not work as expected. @@ -15,11 +15,13 @@ This project is still in development and some things may not work as expected. Paperless does not control your scanner, it only helps you deal with what your scanner produces. -1. Buy a document scanner that can write to a place on your network. If you need some inspiration, have a look at the [scanner recommendations](https://paperless-ng.readthedocs.io/en/latest/scanners.html) page. -2. Set it up to "scan to FTP" or something similar. It should be able to push scanned images to a server without you having to do anything. Of course if your scanner doesn't know how to automatically upload the file somewhere, you can always do that manually. Paperless doesn't care how the documents get into its local consumption directory. -3. Have the target server run the Paperless consumption script to OCR the file and index it into a local database. -4. Use the web frontend to sift through the database and find what you want. -5. Download the PDF you need/want via the web interface and do whatever you like with it. You can even print it and send it as if it's the original. In most cases, no one will care or notice. +1. Buy a document scanner that can write to a place on your network. If you need some inspiration, have a look at the [scanner recommendations](https://paperless-ng.readthedocs.io/en/latest/scanners.html) page. Set it up to "scan to FTP" or something similar. It should be able to push scanned images to a server without you having to do anything. Of course if your scanner doesn't know how to automatically upload the file somewhere, you can always do that manually. Paperless doesn't care how the documents get into its local consumption directory. + + - Alternatively, you can use any of the mobile scanning apps out there. We have an app that allows you to share documents with paperless, if you're on Android. + +2. Wait for paperless to process your files. OCR is expensive, and depending on the power of your machine, this might take a bit of time. +3. Use the web frontend to sift through the database and find what you want. +4. Download the PDF you need/want via the web interface and do whatever you like with it. You can even print it and send it as if it's the original. In most cases, no one will care or notice. Here's what you get: @@ -39,7 +41,6 @@ Here's what you get: * When adding documents from mails, paperless can move these mails to a new folder, mark them as read, flag them or delete them. * Machine learning powered document matching. * Paperless learns from your documents and will be able to automatically assign tags, correspondents and types to documents once you've stored a few documents in paperless. -* We have a mobile app that offers a 'Share with paperless' option over at https://github.com/qcasey/paperless_share. You can use that in combination with any of the mobile scanning apps out there. It's still a little rough around the edges, but it works! * 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. @@ -78,7 +79,7 @@ The recommended way to deploy paperless is docker-compose. Don't clone the repos Read the [documentation](https://paperless-ng.readthedocs.io/en/latest/setup.html#installation) on how to get started. -Alternatively, you can install the dependencies and setup apache and a database server yourself. The documenation has information about the individual components of paperless that you need to take care of. +Alternatively, you can install the dependencies and setup apache and a database server yourself. The documenation has a step by step guide on how to do it. # Migrating to paperless-ng @@ -102,13 +103,15 @@ If you want to implement something big: Please start a discussion about that in Paperless has been around a while now, and people are starting to build stuff on top of it. If you're one of those people, we can add your project to this list: -* [Paperless App](https://github.com/bauerj/paperless_app): An Android/iOS app for Paperless. We're working on making this compatible. +* [Paperless App](https://github.com/bauerj/paperless_app): An Android/iOS app for Paperless. Updated to work with paperless-ng. +* [Paperless Share](https://github.com/qcasey/paperless_share). Share any files from your Android application with paperless. Very simple, but works with all of the mobile scanning apps out there that allow you to share scanned documents. + +These projects also exist, but their status and compatibility with paperless is unknown. + * [Paperless Desktop](https://github.com/thomasbrueggemann/paperless-desktop): A desktop UI for your Paperless installation. Runs on Mac, Linux, and Windows. * [ansible-role-paperless](https://github.com/ovv/ansible-role-paperless): An easy way to get Paperless running via Ansible. * [paperless-cli](https://github.com/stgarf/paperless-cli): A golang command line binary to interact with a Paperless instance. -Compatibility with Paperless-ng is unknown. - # Important Note Document scanners are typically used to scan sensitive documents. Things like your social insurance number, tax records, invoices, etc. Everything is stored in the clear without encryption by default (it needs to be searchable, so if someone has ideas on how to do that on encrypted data, I'm all ears). This means that Paperless should never be run on an untrusted host. Instead, I recommend that if you do want to use it, run it locally on a server in your own home. From 02e67d25b46ae17614a6d2f474564b1c10f0f87f Mon Sep 17 00:00:00 2001 From: Jonas Winkler <dev@jpwinkler.de> Date: Tue, 15 Dec 2020 15:03:34 +0100 Subject: [PATCH 0136/1708] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 276521f5b..95dd83752 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Paperless does not control your scanner, it only helps you deal with what your s 1. Buy a document scanner that can write to a place on your network. If you need some inspiration, have a look at the [scanner recommendations](https://paperless-ng.readthedocs.io/en/latest/scanners.html) page. Set it up to "scan to FTP" or something similar. It should be able to push scanned images to a server without you having to do anything. Of course if your scanner doesn't know how to automatically upload the file somewhere, you can always do that manually. Paperless doesn't care how the documents get into its local consumption directory. - - Alternatively, you can use any of the mobile scanning apps out there. We have an app that allows you to share documents with paperless, if you're on Android. + - Alternatively, you can use any of the mobile scanning apps out there. We have an app that allows you to share documents with paperless, if you're on Android. See the section on affiliated projects. 2. Wait for paperless to process your files. OCR is expensive, and depending on the power of your machine, this might take a bit of time. 3. Use the web frontend to sift through the database and find what you want. From 71c58c4b05b4f4e62f4a6fe24a4f8677accbc570 Mon Sep 17 00:00:00 2001 From: Jonas Winkler <dev@jpwinkler.de> Date: Tue, 15 Dec 2020 15:04:28 +0100 Subject: [PATCH 0137/1708] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 95dd83752..e8ae8feb2 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ Paperless has been around a while now, and people are starting to build stuff on * [Paperless App](https://github.com/bauerj/paperless_app): An Android/iOS app for Paperless. Updated to work with paperless-ng. * [Paperless Share](https://github.com/qcasey/paperless_share). Share any files from your Android application with paperless. Very simple, but works with all of the mobile scanning apps out there that allow you to share scanned documents. -These projects also exist, but their status and compatibility with paperless 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. * [ansible-role-paperless](https://github.com/ovv/ansible-role-paperless): An easy way to get Paperless running via Ansible. From beffde1051669769d1ab38e63372e0f0ba962638 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Tue, 15 Dec 2020 07:10:31 -0800 Subject: [PATCH 0138/1708] quick filter button badges --- .../filter-dropdown.component.html | 15 +++++---------- .../filter-dropdown.component.scss | 6 ++++++ .../filter-editor/filter-editor.component.html | 8 ++++---- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html index 8f7d14e81..d0cbfc3c9 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.html @@ -1,21 +1,16 @@ <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" [ngSwitch]="icon"> - <svg *ngSwitchCase="'person-fill'" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person-fill" viewBox="0 0 16 16"> - <path fill-rule="evenodd" d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z" /> - </svg> - <svg *ngSwitchCase="'tag-fill'" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-tags-fill" viewBox="0 0 16 16"> - <path fill-rule="evenodd" d="M3 1a1 1 0 0 0-1 1v4.586a1 1 0 0 0 .293.707l7 7a1 1 0 0 0 1.414 0l4.586-4.586a1 1 0 0 0 0-1.414l-7-7A1 1 0 0 0 7.586 1H3zm4 3.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z" /> - <path d="M1 7.086a1 1 0 0 0 .293.707L8.75 15.25l-.043.043a1 1 0 0 1-1.414 0l-7-7A1 1 0 0 1 0 7.586V3a1 1 0 0 1 1-1v5.086z" /> - </svg> - <svg *ngSwitchCase="'file-earmark-fill'" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-fill" viewBox="0 0 16 16"> - <path fill-rule="evenodd" d="M4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.707A1 1 0 0 0 13.707 4L10 .293A1 1 0 0 0 9.293 0H4zm5.5 1.5v2a1 1 0 0 0 1 1h2l-3-3z" /> <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"> diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.scss b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.scss index d34729eee..40c93838f 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.scss +++ b/src-ui/src/app/components/filter-editor/filter-dropdown/filter-dropdown.component.scss @@ -1,3 +1,9 @@ +.badge-corner { + position: absolute; + top: -8px; + right: -8px; +} + .dropdown-menu { min-width: 250px; diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.html b/src-ui/src/app/components/filter-editor/filter-editor.component.html index 5bf23f8bd..6847a2902 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.html +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.html @@ -8,10 +8,10 @@ <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" [items]="tags" [itemsSelected]="selectedTags" title="Tags" icon="tag-fill" (toggle)="toggleTag($event.id)"></app-filter-dropdown> - <app-filter-dropdown class="mr-2" [items]="correspondents" [itemsSelected]="selectedCorrespondents" title="Correspondents" icon="person-fill" (toggle)="toggleCorrespondent($event.id)"></app-filter-dropdown> - <app-filter-dropdown class="mr-2" [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" [dateBefore]="dateCreatedBefore" [dateAfter]="dateCreatedAfter" title="Created" (datesSet)="onDatesCreatedSet($event)"></app-filter-dropdown-date> + <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> From b8469946a82b5ddf5253c6cdf9343d45c566a73a Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Tue, 15 Dec 2020 11:09:25 -0800 Subject: [PATCH 0139/1708] Smaller editor, cleaned up responsive flow --- .../bulk-editor/bulk-editor.component.html | 54 +++++++++---------- .../bulk-editor/bulk-editor.component.scss | 6 ++- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html index 54212923a..22724db17 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html @@ -1,67 +1,67 @@ <div class="card bg-light"> -<div class="card-body px-2 py-2 d-flex justify-content-between flex-wrap align-items-end"> - <div class="d-flex flex-grow-1 mb-2 mb-lg-0" role="group" aria-label="Select"> - <label class="d-flex mt-1 mr-auto mr-lg-2">Select:</label> +<div class="card-body small px-2 py-2 d-flex flex-column flex-xl-row justify-content-between justify-content-xl-start"> + <div class="d-flex flex-grow-1 flex-xl-grow-0 mb-2 mb-xl-0 mr-xl-5" role="group" aria-label="Select"> + <label class="d-flex align-self-center my-0 mr-auto mr-lg-2">Select:</label> <div class="btn-group d-flex"> - <button class="btn btn-sm btn-outline-primary" (click)="selectPage.next()"> + <button class="btn btn-sm btn-outline-primary py-1 px-2" (click)="selectPage.next()"> <svg viewBox="0 0 16 16" fill="currentColor"> <use xlink:href="assets/bootstrap-icons.svg#file-earmark-check" /> </svg> - Page + <small>Page</small> </button> - <button class="btn btn-sm btn-outline-primary" (click)="selectAll.next()"> + <button class="btn btn-sm btn-outline-primary py-1 px-2" (click)="selectAll.next()"> <svg viewBox="0 0 16 16" fill="currentColor"> <use xlink:href="assets/bootstrap-icons.svg#check-all" /> </svg> - All + <small>All</small> </button> - <button class="btn btn-sm btn-outline-primary" (click)="selectNone.next()"> + <button class="btn btn-sm btn-outline-primary py-1 px-2" (click)="selectNone.next()"> <svg viewBox="0 0 16 16" fill="currentColor"> <use xlink:href="assets/bootstrap-icons.svg#slash-circle" /> </svg> - None + <small>None</small> </button> </div> </div> - <div class="d-flex flex-grow-1 mb-2 mb-lg-0" role="group" aria-label="Tags"> - <label class="d-flex mt-1 mr-auto mr-lg-2">Tags:</label> + <div class="d-flex flex-grow-1 flex-xl-grow-0 mb-2 mb-xl-0 mr-xl-5" role="group" aria-label="Tags"> + <label class="d-flex align-self-center my-0 mr-auto mr-lg-2">Tags:</label> <div class="btn-group d-flex"> - <button class="btn btn-sm btn-outline-primary" (click)="addTag.next()"> + <button class="btn btn-sm btn-outline-primary py-1 px-2" (click)="addTag.next()"> <ng-container *ngTemplateOutlet="add"></ng-container> </button> - <button class="btn btn-sm btn-outline-primary" (click)="removeTag.next()"> + <button class="btn btn-sm btn-outline-primary py-1 px-2" (click)="removeTag.next()"> <ng-container *ngTemplateOutlet="remove"></ng-container> </button> </div> </div> - <div class="d-flex flex-grow-1 mb-2 mb-lg-0" role="group" aria-label="Correspondent"> - <label class="d-flex mt-1 mr-auto mr-lg-2">Correspondent:</label> + <div class="d-flex flex-grow-1 flex-xl-grow-0 mb-2 mb-xl-0 mr-xl-5" role="group" aria-label="Correspondent"> + <label class="d-flex align-self-center my-0 mr-auto mr-lg-2">Correspondent:</label> <div class="btn-group d-flex"> - <button class="btn btn-sm btn-outline-primary" (click)="setCorrespondent.next()"> + <button class="btn btn-sm btn-outline-primary py-1 px-2" (click)="setCorrespondent.next()"> <ng-container *ngTemplateOutlet="edit"></ng-container> </button> - <button class="btn btn-sm btn-outline-primary" (click)="removeCorrespondent.next()"> + <button class="btn btn-sm btn-outline-primary py-1 px-2" (click)="removeCorrespondent.next()"> <ng-container *ngTemplateOutlet="remove"></ng-container> </button> </div> </div> - <div class="d-flex flex-grow-1 mb-2 mb-lg-0" role="group" aria-label="Document Type"> - <label class="d-flex mt-1 mr-auto mr-lg-2">Document Type:</label> + <div class="d-flex flex-grow-1 flex-xl-grow-0 mb-2 mb-xl-0 mr-xl-5" role="group" aria-label="Document Type"> + <label class="d-flex align-self-center my-0 mr-auto mr-lg-2">Document Type:</label> <div class="btn-group d-flex"> - <button class="btn btn-sm btn-outline-primary" (click)="setDocumentType.next()"> + <button class="btn btn-sm btn-outline-primary py-1 px-2" (click)="setDocumentType.next()"> <ng-container *ngTemplateOutlet="edit"></ng-container> </button> - <button class="btn btn-sm btn-outline-primary" (click)="removeDocumentType.next()"> + <button class="btn btn-sm btn-outline-primary py-1 px-2" (click)="removeDocumentType.next()"> <ng-container *ngTemplateOutlet="remove"></ng-container> </button> </div> </div> - <div class="d-flex flex-grow-1 mb-2 mb-lg-0" role="group" aria-label="Delete"> - <button class="btn btn-sm btn-outline-danger ml-auto" (click)="delete.next()"> + <div class="d-flex flex-grow-1 flex-xl-grow-0 mb-2 mb-lg-0 ml-auto ml-lg-0" role="group" aria-label="Delete"> + <button class="btn btn-sm btn-outline-danger" (click)="delete.next()"> <svg viewBox="0 0 16 16" fill="currentColor"> <use xlink:href="assets/bootstrap-icons.svg#trash" /> </svg> - Delete + <small>Delete</small> </button> </div> </div> @@ -71,19 +71,19 @@ <svg viewBox="0 0 16 16" fill="currentColor"> <use xlink:href="assets/bootstrap-icons.svg#plus-circle" /> </svg> - Add + <small>Add</small> </ng-template> <ng-template #edit> <svg viewBox="0 0 16 16" fill="currentColor"> <use xlink:href="assets/bootstrap-icons.svg#pencil" /> </svg> - Edit + <small>Edit</small> </ng-template> <ng-template #remove> <svg viewBox="0 0 16 16" fill="currentColor"> <use xlink:href="assets/bootstrap-icons.svg#x-circle" /> </svg> - Remove + <small>Remove</small> </ng-template> diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.scss b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.scss index 3868e7a02..5afd86545 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.scss +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.scss @@ -1,6 +1,10 @@ .btn svg { width: 0.9em; height: 0.9em; - margin-right: 2px; + margin-right: 3px; margin-top: -1px; } + +.btn-sm { + line-height: 1; +} From fb9d750684092f40b4d5f5cee565d12ecd15f2f5 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Tue, 15 Dec 2020 14:19:40 -0800 Subject: [PATCH 0140/1708] Delete button margin-left --- .../document-list/bulk-editor/bulk-editor.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html index 22724db17..d330ba228 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html @@ -56,7 +56,7 @@ </button> </div> </div> - <div class="d-flex flex-grow-1 flex-xl-grow-0 mb-2 mb-lg-0 ml-auto ml-lg-0" role="group" aria-label="Delete"> + <div class="d-flex flex-grow-1 flex-xl-grow-0 mb-2 mb-lg-0 ml-auto ml-lg-0 ml-xl-auto" role="group" aria-label="Delete"> <button class="btn btn-sm btn-outline-danger" (click)="delete.next()"> <svg viewBox="0 0 16 16" fill="currentColor"> <use xlink:href="assets/bootstrap-icons.svg#trash" /> From 677cfb7a1e20032b0755a722759e8c88e70c43f2 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Tue, 15 Dec 2020 14:31:18 -0800 Subject: [PATCH 0141/1708] Use bootstrap row-cols-* classes to keep card list view full width --- .../document-card-small/document-card-small.component.html | 6 +++--- .../components/document-list/document-list.component.html | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html index 2647e702c..da0829349 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html @@ -1,4 +1,4 @@ -<div class="col p-2 h-100" style="width: 16rem;"> +<div class="col p-2 h-100"> <div class="card h-100 shadow-sm"> <div class="border-bottom"> <img class="card-img doc-img" [src]="getThumbUrl()"> @@ -22,7 +22,7 @@ </div> <div class="card-footer"> - <div class="d-flex justify-content-between align-items-center ml-n2"> + <div class="d-flex justify-content-between align-items-center mx-n2"> <div class="btn-group"> <a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" title="Edit"> <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> @@ -42,7 +42,7 @@ </svg> </a> </div> - <small class="text-muted">{{document.created | date}}</small> + <small class="text-muted pl-1">{{document.created | date}}</small> </div> </div> diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index 1b3596098..31b00f482 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -116,6 +116,6 @@ </table> -<div class=" m-n2 row" *ngIf="displayMode == 'smallCards'"> +<div class="m-n2 row row row-cols-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-5" *ngIf="displayMode == 'smallCards'"> <app-document-card-small [document]="d" *ngFor="let d of list.documents" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)"></app-document-card-small> </div> From b2a9cf47098f06235e42777baf0cec29ef4807bc Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Tue, 15 Dec 2020 23:35:10 +0100 Subject: [PATCH 0142/1708] docs --- docs/usage_overview.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/usage_overview.rst b/docs/usage_overview.rst index d6f4cf9db..7a4fd7740 100644 --- a/docs/usage_overview.rst +++ b/docs/usage_overview.rst @@ -117,8 +117,8 @@ The mobile app over at `<https://github.com/qcasey/paperless_share>`_ allows And to share any documents with paperless. This can be combined with any of the mobile scanning apps out there, such as Office Lens. -The app is still a little rough around the edges, -but it gets the job done. This will eventually be rolled into `Paperless App <https://github.com/bauerj/paperless_app>`_ as well. +Furthermore, there is the `Paperless App <https://github.com/bauerj/paperless_app>`_ as well, +which no only has document upload, but also document editing and browsing. .. _usage-email: From 22e56f09baf19fd7d3da5d06cbaa247236225fbf Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Wed, 16 Dec 2020 00:14:32 +0100 Subject: [PATCH 0143/1708] fixes some issues regarding #139 --- .../document-card-large/document-card-large.component.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.scss b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.scss index d6be8837e..11fb10562 100644 --- a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.scss +++ b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.scss @@ -1,5 +1,6 @@ .result-content { color: darkgray; + overflow-wrap: anywhere; } .doc-img { From 1a526ac31e230d46f37926facb4d302170a5d057 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Wed, 16 Dec 2020 02:12:58 +0100 Subject: [PATCH 0144/1708] fixes #140 --- .../correspondent-list.component.html | 23 ++++++++++++++++--- .../correspondent-list.component.ts | 14 ++++++++++- .../document-type-list.component.html | 21 +++++++++++++++-- .../document-type-list.component.ts | 14 ++++++++++- .../manage/tag-list/tag-list.component.html | 23 ++++++++++++++++--- .../manage/tag-list/tag-list.component.ts | 15 +++++++++++- 6 files changed, 99 insertions(+), 11 deletions(-) diff --git a/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.html b/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.html index 27aa4d366..2efd1c58d 100644 --- a/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.html +++ b/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.html @@ -26,9 +26,26 @@ <td scope="row">{{ correspondent.last_correspondence | date }}</td> <td scope="row"> <div class="btn-group"> - <button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(correspondent)">Edit</button> - <button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(correspondent)">Delete</button> - </div> + <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"> + <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> + Documents + </button> + <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"> + <path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/> + </svg> + Edit + </button> + <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"> + <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"/> + </svg> + Delete + </button> + </div> </td> </tr> </tbody> diff --git a/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts b/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts index effae2826..a128340b9 100644 --- a/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts +++ b/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts @@ -1,6 +1,9 @@ import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { FILTER_CORRESPONDENT } from 'src/app/data/filter-rule-type'; import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'; +import { DocumentListViewService } from 'src/app/services/document-list-view.service'; import { CorrespondentService } from 'src/app/services/rest/correspondent.service'; import { GenericListComponent } from '../generic-list/generic-list.component'; import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog/correspondent-edit-dialog.component'; @@ -12,7 +15,10 @@ import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog/co }) export class CorrespondentListComponent extends GenericListComponent<PaperlessCorrespondent> { - constructor(correspondentsService: CorrespondentService, modalService: NgbModal,) { + constructor(correspondentsService: CorrespondentService, modalService: NgbModal, + private router: Router, + private list: DocumentListViewService + ) { super(correspondentsService,modalService,CorrespondentEditDialogComponent) } @@ -20,4 +26,10 @@ export class CorrespondentListComponent extends GenericListComponent<PaperlessCo return `correspondent '${object.name}'` } + filterDocuments(object: PaperlessCorrespondent) { + this.list.documentListView.filter_rules = [ + {rule_type: FILTER_CORRESPONDENT, value: object.id.toString()} + ] + this.router.navigate(["documents"]) + } } diff --git a/src-ui/src/app/components/manage/document-type-list/document-type-list.component.html b/src-ui/src/app/components/manage/document-type-list/document-type-list.component.html index 78c86daf3..d2ffab400 100644 --- a/src-ui/src/app/components/manage/document-type-list/document-type-list.component.html +++ b/src-ui/src/app/components/manage/document-type-list/document-type-list.component.html @@ -25,8 +25,25 @@ <td scope="row">{{ document_type.document_count }}</td> <td scope="row"> <div class="btn-group"> - <button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(document_type)">Edit</button> - <button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(document_type)">Delete</button> + <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"> + <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> + Documents + </button> + <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"> + <path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/> + </svg> + Edit + </button> + <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"> + <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"/> + </svg> + Delete + </button> </div> </td> </tr> diff --git a/src-ui/src/app/components/manage/document-type-list/document-type-list.component.ts b/src-ui/src/app/components/manage/document-type-list/document-type-list.component.ts index 16cdd88a9..d18a19226 100644 --- a/src-ui/src/app/components/manage/document-type-list/document-type-list.component.ts +++ b/src-ui/src/app/components/manage/document-type-list/document-type-list.component.ts @@ -1,6 +1,9 @@ import { Component } from '@angular/core'; +import { Router } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { FILTER_DOCUMENT_TYPE } from 'src/app/data/filter-rule-type'; import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'; +import { DocumentListViewService } from 'src/app/services/document-list-view.service'; import { DocumentTypeService } from 'src/app/services/rest/document-type.service'; import { GenericListComponent } from '../generic-list/generic-list.component'; import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog/document-type-edit-dialog.component'; @@ -12,7 +15,10 @@ import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog/doc }) export class DocumentTypeListComponent extends GenericListComponent<PaperlessDocumentType> { - constructor(service: DocumentTypeService, modalService: NgbModal) { + constructor(service: DocumentTypeService, modalService: NgbModal, + private router: Router, + private list: DocumentListViewService + ) { super(service, modalService, DocumentTypeEditDialogComponent) } @@ -20,4 +26,10 @@ export class DocumentTypeListComponent extends GenericListComponent<PaperlessDoc return `document type '${object.name}'` } + filterDocuments(object: PaperlessDocumentType) { + this.list.documentListView.filter_rules = [ + {rule_type: FILTER_DOCUMENT_TYPE, value: object.id.toString()} + ] + this.router.navigate(["documents"]) + } } diff --git a/src-ui/src/app/components/manage/tag-list/tag-list.component.html b/src-ui/src/app/components/manage/tag-list/tag-list.component.html index e68b997d1..bbe2c6dd2 100644 --- a/src-ui/src/app/components/manage/tag-list/tag-list.component.html +++ b/src-ui/src/app/components/manage/tag-list/tag-list.component.html @@ -9,7 +9,7 @@ aria-label="Default pagination"></ngb-pagination> </div> -<table class="table table-striped border shadow"> +<table class="table table-striped border shadow-sm"> <thead> <tr> <th scope="col" sortable="name" (sort)="onSort($event)">Name</th> @@ -28,8 +28,25 @@ <td scope="row">{{ tag.document_count }}</td> <td scope="row"> <div class="btn-group"> - <button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(tag)">Edit</button> - <button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(tag)">Delete</button> + <button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(tag)"> + <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"/> + </svg> + Documents + </button> + <button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(tag)"> + <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> + <path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/> + </svg> + Edit + </button> + <button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(tag)"> + <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 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> + Delete + </button> </div> </td> </tr> diff --git a/src-ui/src/app/components/manage/tag-list/tag-list.component.ts b/src-ui/src/app/components/manage/tag-list/tag-list.component.ts index 32093e0a8..e3f151550 100644 --- a/src-ui/src/app/components/manage/tag-list/tag-list.component.ts +++ b/src-ui/src/app/components/manage/tag-list/tag-list.component.ts @@ -1,6 +1,9 @@ import { Component } from '@angular/core'; +import { Router } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { FILTER_HAS_TAG } from 'src/app/data/filter-rule-type'; import { TAG_COLOURS, PaperlessTag } from 'src/app/data/paperless-tag'; +import { DocumentListViewService } from 'src/app/services/document-list-view.service'; import { TagService } from 'src/app/services/rest/tag.service'; import { GenericListComponent } from '../generic-list/generic-list.component'; import { TagEditDialogComponent } from './tag-edit-dialog/tag-edit-dialog.component'; @@ -12,7 +15,10 @@ import { TagEditDialogComponent } from './tag-edit-dialog/tag-edit-dialog.compon }) export class TagListComponent extends GenericListComponent<PaperlessTag> { - constructor(tagService: TagService, modalService: NgbModal) { + constructor(tagService: TagService, modalService: NgbModal, + private router: Router, + private list: DocumentListViewService + ) { super(tagService, modalService, TagEditDialogComponent) } @@ -23,4 +29,11 @@ export class TagListComponent extends GenericListComponent<PaperlessTag> { getObjectName(object: PaperlessTag) { return `tag '${object.name}'` } + + filterDocuments(object: PaperlessTag) { + this.list.documentListView.filter_rules = [ + {rule_type: FILTER_HAS_TAG, value: object.id.toString()} + ] + this.router.navigate(["documents"]) + } } From e528a587cc89e6529479d34d6fa3f8a53b9b52bc Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Wed, 16 Dec 2020 13:41:02 +0100 Subject: [PATCH 0145/1708] fixed some issues with the test cases. --- src/paperless_mail/tests/test_mail.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/paperless_mail/tests/test_mail.py b/src/paperless_mail/tests/test_mail.py index 2a391a268..9c0f52c53 100644 --- a/src/paperless_mail/tests/test_mail.py +++ b/src/paperless_mail/tests/test_mail.py @@ -399,7 +399,7 @@ class TestMail(TestCase): c = Correspondent.objects.get(name="amazon@amazon.de") # should work - self.assertEquals(kwargs['override_correspondent_id'], c.id) + self.assertEqual(kwargs['override_correspondent_id'], c.id) self.async_task.reset_mock() self.reset_bogus_mailbox() @@ -411,7 +411,7 @@ class TestMail(TestCase): args, kwargs = self.async_task.call_args self.async_task.assert_called_once() - self.assertEquals(kwargs['override_correspondent_id'], None) + self.assertEqual(kwargs['override_correspondent_id'], None) def test_filters(self): From 8bd82f5c69fa4de1ac02d4c29741253ee30311ce Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Wed, 16 Dec 2020 13:49:48 +0100 Subject: [PATCH 0146/1708] fixing some test case warnings, case insensitive sorting for tags, correspondents and types. --- src/documents/models.py | 7 ++++++- src/documents/tests/test_file_handling.py | 11 ++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/documents/models.py b/src/documents/models.py index d81343afa..245bba6e9 100755 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -11,6 +11,7 @@ import dateutil.parser from django.conf import settings from django.contrib.auth.models import User from django.db import models +from django.db.models.functions import Lower from django.utils import timezone from django.utils.text import slugify @@ -61,7 +62,7 @@ class MatchingModel(models.Model): class Meta: abstract = True - ordering = ("name",) + ordering = (Lower("name"),) def __str__(self): return self.name @@ -308,6 +309,10 @@ class Log(models.Model): class SavedView(models.Model): + class Meta: + + ordering = (Lower("name"),) + user = models.ForeignKey(User, on_delete=models.CASCADE) name = models.CharField(max_length=128) diff --git a/src/documents/tests/test_file_handling.py b/src/documents/tests/test_file_handling.py index dec89c45b..2e60065f1 100644 --- a/src/documents/tests/test_file_handling.py +++ b/src/documents/tests/test_file_handling.py @@ -9,6 +9,7 @@ from unittest import mock from django.conf import settings from django.db import DatabaseError from django.test import TestCase, override_settings +from django.utils import timezone from .utils import DirectoriesMixin from ..file_handling import generate_filename, create_source_path_directory, delete_empty_directories, \ @@ -298,23 +299,23 @@ class TestFileHandling(DirectoriesMixin, TestCase): @override_settings(PAPERLESS_FILENAME_FORMAT="{created_year}-{created_month}-{created_day}") def test_created_year_month_day(self): - d1 = datetime.datetime(2020, 3, 6, 1, 1, 1) + d1 = timezone.make_aware(datetime.datetime(2020, 3, 6, 1, 1, 1)) doc1 = Document.objects.create(title="doc1", mime_type="application/pdf", created=d1) self.assertEqual(generate_filename(doc1), "2020-03-06.pdf") - doc1.created = datetime.datetime(2020, 11, 16, 1, 1, 1) + doc1.created = timezone.make_aware(datetime.datetime(2020, 11, 16, 1, 1, 1)) self.assertEqual(generate_filename(doc1), "2020-11-16.pdf") @override_settings(PAPERLESS_FILENAME_FORMAT="{added_year}-{added_month}-{added_day}") def test_added_year_month_day(self): - d1 = datetime.datetime(232, 1, 9, 1, 1, 1) + d1 = timezone.make_aware(datetime.datetime(232, 1, 9, 1, 1, 1)) doc1 = Document.objects.create(title="doc1", mime_type="application/pdf", added=d1) self.assertEqual(generate_filename(doc1), "232-01-09.pdf") - doc1.added = datetime.datetime(2020, 11, 16, 1, 1, 1) + doc1.added = timezone.make_aware(datetime.datetime(2020, 11, 16, 1, 1, 1)) self.assertEqual(generate_filename(doc1), "2020-11-16.pdf") @@ -599,7 +600,7 @@ class TestFilenameGeneration(TestCase): PAPERLESS_FILENAME_FORMAT="{created}" ) def test_date(self): - doc = Document.objects.create(title="does not matter", created=datetime.datetime(2020,5,21, 7,36,51, 153), mime_type="application/pdf", pk=2, checksum="2") + doc = Document.objects.create(title="does not matter", created=timezone.make_aware(datetime.datetime(2020,5,21, 7,36,51, 153)), mime_type="application/pdf", pk=2, checksum="2") self.assertEqual(generate_filename(doc), "2020-05-21.pdf") From e47b105185912efc08160d9ee28dc57dae58ef3d Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Wed, 16 Dec 2020 14:17:05 +0100 Subject: [PATCH 0147/1708] fixes #7 and some test cases. --- src/paperless_text/parsers.py | 60 ++++++----------------------------- 1 file changed, 10 insertions(+), 50 deletions(-) diff --git a/src/paperless_text/parsers.py b/src/paperless_text/parsers.py index f8f369ab0..646c5c549 100644 --- a/src/paperless_text/parsers.py +++ b/src/paperless_text/parsers.py @@ -1,6 +1,7 @@ import os import subprocess +from PIL import ImageDraw, ImageFont, Image from django.conf import settings from documents.parsers import DocumentParser, ParseError @@ -12,63 +13,22 @@ class TextDocumentParser(DocumentParser): """ def get_thumbnail(self, document_path, mime_type): - """ - The thumbnail of a text file is just a 500px wide image of the text - rendered onto a letter-sized page. - """ - # The below is heavily cribbed from https://askubuntu.com/a/590951 - - bg_color = "white" # bg color - text_color = "black" # text color - psize = [500, 647] # icon size - n_lines = 50 # number of lines to show - out_path = os.path.join(self.tempdir, "convert.png") - - temp_bg = os.path.join(self.tempdir, "bg.png") - temp_txlayer = os.path.join(self.tempdir, "tx.png") - picsize = "x".join([str(n) for n in psize]) - txsize = "x".join([str(n - 8) for n in psize]) - - def create_bg(): - work_size = ",".join([str(n - 1) for n in psize]) - r = str(round(psize[0] / 10)) - rounded = ",".join([r, r]) - run_command( - settings.CONVERT_BINARY, - "-size ", picsize, - ' xc:none -draw ', - '"fill ', bg_color, ' roundrectangle 0,0,', work_size, ",", rounded, '" ', # NOQA: E501 - temp_bg - ) def read_text(): with open(document_path, 'r') as src: lines = [line.strip() for line in src.readlines()] - text = "\n".join([line for line in lines[:n_lines]]) + text = "\n".join([line for line in lines[:50]]) return text.replace('"', "'") - def create_txlayer(): - run_command( - settings.CONVERT_BINARY, - "-background none", - "-fill", - text_color, - "-pointsize", "12", - "-border 4 -bordercolor none", - "-size ", txsize, - ' caption:"', read_text(), '" ', - temp_txlayer - ) + img = Image.new("RGB", (500, 700), color="white") + draw = ImageDraw.Draw(img) + font = ImageFont.truetype( + "/usr/share/fonts/liberation/LiberationSerif-Regular.ttf", 20, + layout_engine=ImageFont.LAYOUT_BASIC) + draw.text((5, 5), read_text(), font=font, fill="black") - create_txlayer() - create_bg() - run_command( - settings.CONVERT_BINARY, - temp_bg, - temp_txlayer, - "-background None -layers merge ", - out_path - ) + out_path = os.path.join(self.tempdir, "thumb.png") + img.save(out_path) return out_path From b2e0a8c88401c33077192ce6285057e60e1afd74 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Wed, 16 Dec 2020 14:19:11 +0100 Subject: [PATCH 0148/1708] thumbnail generation --- src/paperless_text/parsers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/paperless_text/parsers.py b/src/paperless_text/parsers.py index 646c5c549..7e488ca37 100644 --- a/src/paperless_text/parsers.py +++ b/src/paperless_text/parsers.py @@ -17,8 +17,8 @@ class TextDocumentParser(DocumentParser): def read_text(): with open(document_path, 'r') as src: lines = [line.strip() for line in src.readlines()] - text = "\n".join([line for line in lines[:50]]) - return text.replace('"', "'") + text = "\n".join(lines[:50]) + return text img = Image.new("RGB", (500, 700), color="white") draw = ImageDraw.Draw(img) From 9f5e6d1969f5af9742e0399414a297f81324021c Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Wed, 16 Dec 2020 14:23:10 +0100 Subject: [PATCH 0149/1708] fixes metadata display --- .../components/document-detail/document-detail.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html index c0114f709..f4a64c2cc 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.html +++ b/src-ui/src/app/components/document-detail/document-detail.component.html @@ -110,8 +110,8 @@ </tbody> </table> - <app-metadata-collapse title="Original document metadata" [metadata]="metadata.original_metadata" *ngIf="metadata?.original_metadata.length > 0"></app-metadata-collapse> - <app-metadata-collapse title="Archived document metadata" [metadata]="metadata.archive_metadata" *ngIf="metadata?.archive_metadata.length > 0"></app-metadata-collapse> + <app-metadata-collapse 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> </ng-template> </li> From b13ec571f84cef60bd54dab2f59b61650a435b92 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Wed, 16 Dec 2020 14:41:57 +0100 Subject: [PATCH 0150/1708] fixes #134 --- docs/changelog.rst | 2 ++ src-ui/src/app/components/app-frame/app-frame.component.html | 5 +++++ src-ui/src/app/components/app-frame/app-frame.component.ts | 3 +++ src-ui/src/environments/environment.prod.ts | 3 ++- src-ui/src/environments/environment.ts | 3 ++- 5 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 84d04bc7a..b508bae13 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -28,6 +28,7 @@ paperless-ng 0.9.7 * The ``document_retagger`` no longer removes inbox tags or tags without matching rules. * The new configuration option ``PAPERLESS_COOKIE_PREFIX`` allows you to run multiple instances of paperless on different ports. This option enables you to be logged in into multiple instances by specifying different cookie names for each instance. + * Added a small version indicator to the front end. * Fixes @@ -38,6 +39,7 @@ paperless-ng 0.9.7 Paperless now assumes A4 paper size for PDF generation if no DPI information is present. * Documents with empty titles could not be opened from the table view due to the link being empty. * Fixed an issue with filenames containing special characters such as ``:`` not being accepted for upload. + * Fixed issues with thumbnail generation for plain text files. paperless-ng 0.9.6 diff --git a/src-ui/src/app/components/app-frame/app-frame.component.html b/src-ui/src/app/components/app-frame/app-frame.component.html index 7876150af..3d315ec32 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.html +++ b/src-ui/src/app/components/app-frame/app-frame.component.html @@ -17,6 +17,11 @@ <div class="container-fluid"> <div class="row"> <nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse" [ngbCollapse]="isMenuCollapsed"> + + <div style="position: absolute; bottom: 0; left: 0;" class="text-muted p-1"> + {{versionString}} + </div> + <div class="sidebar-sticky pt-3"> <ul class="nav flex-column"> <li class="nav-item"> diff --git a/src-ui/src/app/components/app-frame/app-frame.component.ts b/src-ui/src/app/components/app-frame/app-frame.component.ts index ef859bf35..c4c00843d 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.ts +++ b/src-ui/src/app/components/app-frame/app-frame.component.ts @@ -7,6 +7,7 @@ import { PaperlessDocument } from 'src/app/data/paperless-document'; import { OpenDocumentsService } from 'src/app/services/open-documents.service'; import { SavedViewService } from 'src/app/services/rest/saved-view.service'; import { SearchService } from 'src/app/services/rest/search.service'; +import { environment } from 'src/environments/environment'; import { DocumentDetailComponent } from '../document-detail/document-detail.component'; @Component({ @@ -25,6 +26,8 @@ export class AppFrameComponent implements OnInit, OnDestroy { ) { } + versionString = `${environment.appTitle} ${environment.version}` + isMenuCollapsed: boolean = true closeMenu() { diff --git a/src-ui/src/environments/environment.prod.ts b/src-ui/src/environments/environment.prod.ts index 09154dfca..38699670e 100644 --- a/src-ui/src/environments/environment.prod.ts +++ b/src-ui/src/environments/environment.prod.ts @@ -1,5 +1,6 @@ export const environment = { production: true, apiBaseUrl: "/api/", - appTitle: "Paperless-ng" + appTitle: "Paperless-ng", + version: "0.9.7" }; diff --git a/src-ui/src/environments/environment.ts b/src-ui/src/environments/environment.ts index 5e4b148dc..29a8f3af6 100644 --- a/src-ui/src/environments/environment.ts +++ b/src-ui/src/environments/environment.ts @@ -5,7 +5,8 @@ export const environment = { production: false, apiBaseUrl: "http://localhost:8000/api/", - appTitle: "DEVELOPMENT P-NG" + appTitle: "Paperless-ng", + version: "DEVELOPMENT" }; /* From 0e78f32009ae97d7b91531f2dabb3b5bd210fc29 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Wed, 16 Dec 2020 16:04:20 +0100 Subject: [PATCH 0151/1708] fixed an issue with the settings not saving in case no saved views are present --- .../manage/settings/settings.component.ts | 20 ++++++++++++++----- .../services/document-list-view.service.ts | 4 ++-- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src-ui/src/app/components/manage/settings/settings.component.ts b/src-ui/src/app/components/manage/settings/settings.component.ts index 41bb21156..f839010b1 100644 --- a/src-ui/src/app/components/manage/settings/settings.component.ts +++ b/src-ui/src/app/components/manage/settings/settings.component.ts @@ -50,16 +50,26 @@ export class SettingsComponent implements OnInit { }) } + private saveLocalSettings() { + localStorage.setItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE, this.settingsForm.value.documentListItemPerPage) + this.documentListViewService.updatePageSize() + this.toastService.showToast(Toast.make("Information", "Settings saved successfully.")) + } + saveSettings() { let x = [] for (let id in this.savedViewGroup.value) { x.push(this.savedViewGroup.value[id]) } - this.savedViewService.patchMany(x).subscribe(s => { - this.toastService.showToast(Toast.make("Information", "Settings saved successfully.")) - localStorage.setItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE, this.settingsForm.value.documentListItemPerPage) - this.documentListViewService.updatePageSize() - }) + if (x.length > 0) { + this.savedViewService.patchMany(x).subscribe(s => { + this.saveLocalSettings() + }, error => { + this.toastService.showToast(Toast.makeError(`Error while storing settings on server: ${JSON.stringify(error.error)}`)) + }) + } else { + this.saveLocalSettings() + } } } diff --git a/src-ui/src/app/services/document-list-view.service.ts b/src-ui/src/app/services/document-list-view.service.ts index 7405fcd24..a549f373d 100644 --- a/src-ui/src/app/services/document-list-view.service.ts +++ b/src-ui/src/app/services/document-list-view.service.ts @@ -116,13 +116,13 @@ export class DocumentListViewService { set filterRules(filterRules: FilterRule[]) { //we're going to clone the filterRules object, since we don't //want changes in the filter editor to propagate into here right away. - this.view.filter_rules = cloneFilterRules(filterRules) + this.view.filter_rules = filterRules this.reload() this.saveDocumentListView() } get filterRules(): FilterRule[] { - return cloneFilterRules(this.view.filter_rules) + return this.view.filter_rules } set sortField(field: string) { From 6b60501dc7dd060df8ba68b999a7c14fe46ad410 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Wed, 16 Dec 2020 16:04:25 +0100 Subject: [PATCH 0152/1708] changelog --- docs/changelog.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b508bae13..4c72b45bf 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -17,10 +17,12 @@ paperless-ng 0.9.7 * `Michael Shamoon`_ replaced the document preview with another component. This should fix compatibility with Safari browsers. + * Added buttons to the management pages to quickly show all documents with one specific tag, correspondent, or title. + * Paperless now stores your saved views on the server and associates them with your user account. This means that you can access your views on multiple devices and have separate views for different users. You will have to recreate your views. - + * Other additions and changes * The GitHub and documentation links now open in new tabs/windows. Thanks to `rYR79435`_. From dec17a3b9b92b5db8f76d7647cf497ef9273f53b Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Wed, 16 Dec 2020 16:19:12 +0100 Subject: [PATCH 0153/1708] fixes a one time issue when migrating to the new version. --- src-ui/src/app/services/document-list-view.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-ui/src/app/services/document-list-view.service.ts b/src-ui/src/app/services/document-list-view.service.ts index a549f373d..57d0a3f0e 100644 --- a/src-ui/src/app/services/document-list-view.service.ts +++ b/src-ui/src/app/services/document-list-view.service.ts @@ -201,7 +201,7 @@ export class DocumentListViewService { this.documentListView = null } } - if (!this.documentListView) { + if (!this.documentListView || !this.documentListView.filter_rules || !this.documentListView.sort_reverse || !this.documentListView.sort_field) { this.documentListView = { filter_rules: [], sort_reverse: true, From 8e339789faea8fb32a9113dfaf649fae22f0b8f3 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Wed, 16 Dec 2020 16:44:54 +0100 Subject: [PATCH 0154/1708] more fixes regarding empty titles --- src-ui/src/app/app.module.ts | 3 ++- .../src/app/components/app-frame/app-frame.component.html | 2 +- .../saved-view-widget/saved-view-widget.component.html | 2 +- .../components/document-detail/document-detail.component.ts | 6 ++++-- src-ui/src/app/pipes/document-title.pipe.ts | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index f935b7701..3c00cd0b7 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -119,7 +119,8 @@ import { MetadataCollapseComponent } from './components/document-detail/metadata useClass: CsrfInterceptor, multi: true }, - FilterPipe + FilterPipe, + DocumentTitlePipe ], bootstrap: [AppComponent] }) diff --git a/src-ui/src/app/components/app-frame/app-frame.component.html b/src-ui/src/app/components/app-frame/app-frame.component.html index 3d315ec32..2458005f4 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.html +++ b/src-ui/src/app/components/app-frame/app-frame.component.html @@ -65,7 +65,7 @@ <svg class="sidebaricon" fill="currentColor"> <use xlink:href="assets/bootstrap-icons.svg#file-text"/> </svg> - {{d.title}} + {{d.title | documentTitle}} </a> </li> <li class="nav-item w-100" *ngIf="openDocuments.length > 1"> diff --git a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html index 194497d39..f50708af3 100644 --- a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html +++ b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html @@ -13,7 +13,7 @@ <tbody> <tr *ngFor="let doc of documents" routerLink="/documents/{{doc.id}}"> <td>{{doc.created | date}}</td> - <td>{{doc.title}}<app-tag [tag]="t" *ngFor="let t of doc.tags$ | async" class="ml-1"></app-tag> + <td>{{doc.title | documentTitle}}<app-tag [tag]="t" *ngFor="let t of doc.tags$ | async" class="ml-1"></app-tag> </tr> </tbody> </table> diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index 5fe9f9250..b4005b920 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -6,6 +6,7 @@ import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'; import { PaperlessDocument } from 'src/app/data/paperless-document'; import { PaperlessDocumentMetadata } from 'src/app/data/paperless-document-metadata'; import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'; +import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'; import { DocumentListViewService } from 'src/app/services/document-list-view.service'; import { OpenDocumentsService } from 'src/app/services/open-documents.service'; import { CorrespondentService } from 'src/app/services/rest/correspondent.service'; @@ -54,7 +55,8 @@ export class DocumentDetailComponent implements OnInit { private router: Router, private modalService: NgbModal, private openDocumentService: OpenDocumentsService, - private documentListViewService: DocumentListViewService) { } + private documentListViewService: DocumentListViewService, + private documentTitlePipe: DocumentTitlePipe) { } getContentType() { return this.metadata?.has_archive_version ? 'application/pdf' : this.metadata?.original_mime_type @@ -90,7 +92,7 @@ export class DocumentDetailComponent implements OnInit { this.documentsService.getMetadata(doc.id).subscribe(result => { this.metadata = result }) - this.title = doc.title + this.title = this.documentTitlePipe.transform(doc.title) this.documentForm.patchValue(doc) } diff --git a/src-ui/src/app/pipes/document-title.pipe.ts b/src-ui/src/app/pipes/document-title.pipe.ts index 09445f595..621562d39 100644 --- a/src-ui/src/app/pipes/document-title.pipe.ts +++ b/src-ui/src/app/pipes/document-title.pipe.ts @@ -5,7 +5,7 @@ import { Pipe, PipeTransform } from '@angular/core'; }) export class DocumentTitlePipe implements PipeTransform { - transform(value: string): unknown { + transform(value: string): string { if (value) { return value } else { From 28f45d8f159d82e68fe1b10ef2127065de95966a Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Wed, 16 Dec 2020 16:59:26 +0100 Subject: [PATCH 0155/1708] fixes an issue with the date dropdowns --- .../filter-dropdown-date/filter-dropdown-date.component.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts index 806027f9c..91402d084 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.ts @@ -1,7 +1,6 @@ import { Component, EventEmitter, Input, Output, ElementRef, ViewChild, SimpleChange } from '@angular/core'; import { NgbDate, NgbDateStruct, NgbDatepicker } from '@ng-bootstrap/ng-bootstrap'; - export interface DateSelection { before?: NgbDateStruct after?: NgbDateStruct @@ -72,7 +71,6 @@ export class FilterDropdownDateComponent { } setDateQuickFilter(range: any) { - this._dateAfter = this._dateBefore = undefined let date = new Date() let newDate: NgbDateStruct = { year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate() } switch (typeof range) { @@ -92,18 +90,23 @@ export class FilterDropdownDateComponent { break } this._dateAfter = newDate + this._dateBefore = null this.datesSet.emit({after: newDate, before: null}) } onBeforeSelected(date: NgbDateStruct) { + this._dateBefore = date this.datesSet.emit({after: this._dateAfter, before: date}) } onAfterSelected(date: NgbDateStruct) { + this._dateAfter = date this.datesSet.emit({after: date, before: this._dateBefore}) } clear() { + this._dateBefore = null + this._dateAfter = null this.datesSet.emit({after: null, before: null}) } } From c813c020253e8dc03db85ebd501ed4c2e7edc7e9 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Wed, 16 Dec 2020 17:01:20 +0100 Subject: [PATCH 0156/1708] version increment. --- docker/hub/docker-compose.postgres.yml | 2 +- docker/hub/docker-compose.sqlite.yml | 2 +- scripts/make-release.sh | 1 + src/paperless/version.py | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docker/hub/docker-compose.postgres.yml b/docker/hub/docker-compose.postgres.yml index 24f0e118f..6ab4b94a6 100644 --- a/docker/hub/docker-compose.postgres.yml +++ b/docker/hub/docker-compose.postgres.yml @@ -15,7 +15,7 @@ services: POSTGRES_PASSWORD: paperless webserver: - image: jonaswinkler/paperless-ng:0.9.6 + image: jonaswinkler/paperless-ng:0.9.7 restart: always depends_on: - db diff --git a/docker/hub/docker-compose.sqlite.yml b/docker/hub/docker-compose.sqlite.yml index 6ae619fd6..4e1da3e10 100644 --- a/docker/hub/docker-compose.sqlite.yml +++ b/docker/hub/docker-compose.sqlite.yml @@ -5,7 +5,7 @@ services: restart: always webserver: - image: jonaswinkler/paperless-ng:0.9.6 + image: jonaswinkler/paperless-ng:0.9.7 restart: always depends_on: - broker diff --git a/scripts/make-release.sh b/scripts/make-release.sh index 0a7bc7a9b..f5c9028fa 100755 --- a/scripts/make-release.sh +++ b/scripts/make-release.sh @@ -5,6 +5,7 @@ # adjust src/paperless/version.py # changelog in the documentation # adjust versions in docker/hub/* +# adjust version in src-ui/src/environments/prod # If docker-compose was modified: all compose files are the same. # Steps: diff --git a/src/paperless/version.py b/src/paperless/version.py index 527e0668d..3c8636a10 100644 --- a/src/paperless/version.py +++ b/src/paperless/version.py @@ -1 +1 @@ -__version__ = (0, 9, 6) +__version__ = (0, 9, 7) From 062f8e5a730b248e638742588dceea16f27968eb Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Wed, 16 Dec 2020 17:47:34 +0100 Subject: [PATCH 0157/1708] update save filter picture --- src-ui/src/assets/save-filter.png | Bin 8267 -> 8263 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src-ui/src/assets/save-filter.png b/src-ui/src/assets/save-filter.png index dcaa41714c78a70b8568e162a0e8afac86fe8168..0f011f8127280d88db5970d30e23570765fa5cc9 100644 GIT binary patch literal 8263 zcmc&)Wl&t*vc-eDGe~d-m*7rtg1c+*K=5D_f&_PWcPF?64emZT!C`O)hsSs8)vb4{ z-uwMd)$YCO{MfZmpX%PdR_`!XWmybVQdAfi7z}y2kLqvp&fAznhI_N{D#V^|6N<B( zo+}Itmf1f8wp6Ud76yj$UjCzmrk61!3&j`DYJJc;e(%6<<K0v;96@aIqv2OQ0_=~{ z#_rOK@VZGxo+d<2($E`dLt7U`d+4I~b_65E=71lK+$aVjcxGlMkk@vW$1(l$I*5DN zjq8>+fnFn<9Xosf^(n_<?|ygqYD_766G6h%A3m5Lg+a#jTp`5AI7=Ds_<qvrJ^2uA zih)YKkP>)UICkI=HGbC%^}SsCVA}cI_T^3WfJW%bDO=jumIu{G`@Hy}S0j2Ap*5`= zLP;UJdIg3n2Y#Oj^Ir+K4*X2k7arbrW&m3!R)BS&M|s*V<NGl2TpU+%DoI3I?I1gG zbLmo~e*iL`Yb<K?76PV*2l_i?0HM*j*hfyo*@SI3I}Qwf8`Ec)u=Dc6uL1I6X&9c8 zY@xC2q%H(`0f^uKH1bTfEWnc9&g6OGNbr|gVfgupBMs4E`rU|oQVFAx=6fRUH3^|i zJ_Oq4Em&0(%>)?!jbmEJ?DRO3Mkk>&kTco6rU+TtkT-DomcGT-(Y%{F_))sdb$cFH zg>1Eg$D6GL@2WChzFpVpmg!Tm4A*y=|GDvhf5HDB7~sY8aEv%c#jcLBFZ*r{(m(Q) zo2qG?S_$*7k`}bF{BIZkdxPCcS)XPVEfQG(1Rk+kiDGIjayJivBzJz70zawAnujU4 zmUmGyQi!W&Rx1q76w8EqYHB~brAY{_$U=shgmblA1rab63a%L^q)kGT$O+uBZ>$Ys z;d2H+Kf7S}N%WYC)N29=D%1%lWWx92&2ijSN2rge*2aIv7wO(b`oS30xDC%(!}Z`) zxE0_ls;)kW7JTFjBN2!i0ZI_IY;J-*&379Vg#+O{!v2Ud&rXIHTBn>2+a@M;I$;x3 zkPT!o9vcq1B-^L#vW&7h&%iC>>P`51=uP+`8B`LNSi(;$P);C^Q1i+6umntf|Hzm& zQ}F)1_7*Kj-EV?16MRb&tnm*ZMaGc(s9Q-aRS#=dYEu{Bfj!PPFsSlw`b6FLsj?UJ zv%5zXxF)FsBtTBrm6u%j6pj^!ufim%YtxR1{LLG~&|o(ip!(V^V8Qil_21_586H8Z z-l5YxfRH&%Z1*v9aX&JZ@|#v7&K0}UW^P5YMJOag=FtQV7(k1!ph^f;g88fAsG3gE zq5!YM6@q#uB`H^lFHzQc#teBRt)tQ^$Ion7qKZgLz;?iuAL2zvgEZZvYt$c!$lWv< z5~d89p-Po&*VUNQV59bS9!WYG9_G3d7)}k9dzT+olpj|Zfc1^einE&&9_I;)dI2Dg zk-i&x+Eu+E;1!Ors303VGrJhG$yh>4SNjB547t%25wiT%Ms_|JI94@lT!))@UCJTD z;me^=$@2AG=+ntl8lJTskNR(oH4ve0D0z`k`B8a?<$jgor0O!WDtjHSF&qTG&YZ1t zB3syCEP~9c7&gd0hPGxcYbYv-<3|@7!QL`<xQ{18uRT2d4{J|`vCe6Hy*awNMJ;tn z1#k6*oG&j8Y9%ET0>0OQ>hCuXPQhAj4X5T+4ecwBaz6Gbr^Y%Q;-3pYn}}SH8l}D{ z-K25vPu}rTs>)POk+N0?e<z#`zYjq$`SN1^5{YPT+;u)C#N2OaIlH9kbZi6iyAexa z^$q39cH&n-@*oG#FB#Uf^x`9cJ@U4mVto8tJdO-QvqeA)e02(0B2+&jimDomyVAed zHtG+FTWFA@#~P+dBHNf%l|0i&=q;sT(Q406O&Oj!I%5^(2^Y`gHO>wq(bQ3E!s9Ya zexUlFAdqWO?~#%|f=N^``)$3Z>{5PF|2u>(7Tn=cCVszDe1h@uiAh%@x1fQ`5b}-7 zeW@Vm5ZJ~p=(o8stl7qN=goRbxWcFcW>V6_a`xO#r1TZx#s?>ywa~x19(KypvD96L zsOD2YL03me1?!A^#NmwW+9k8LXZ4;-LSP|qJHW{N^b6R*F$=29S!1fMrXgBn1Fj3V zFpusiq_bZAuz!m7iAa50<4f1xZDC!<!&no%0j>p}=pEE=>!f~Yoayf5{sU=8GbjRW zu&d!IP`MqFZG9TFjx$77&nS#R#gV^qI2C+2v~N{tQiEhA8!Q|4ElgF<Kuyg+;y0Nn zBe$nB)U?ibJ8?0w4_S9s)wL_{A$*&;f3-C%CJc18;%WUU`xz2Y^AiJDc*0#W#>UEA z<#^sEvCFWlWwpoaI$b;cF8Y*))wOuSsM(Pmfi#_%kLbyK_>&@K%ROw{ZsUASmEjdq z&TlbBk@~j&;k!S4rpLc}GHZ&eisr%FZXSV%G8++l5#v?k7~?nR;N|7%xf#vL4*5WV z%;@qq*OCgAoN8d8r0G(Kf1}WeCD7)ecK|K%syk8wo*PMhdbLtmIuSK-F&T@T-(e}& z%sZ>AXO3!bVmz#2@QBR*{nWD6tM{=^A<kYUUWf`yg<x6=H~%Uzo9=hvg)>93*(+ZS z%a3zQW)^&qN1Waj9SD6CO$dIK?`7w$w(qaYFi-B8`k#9y(0fIbczLXio4Z#QzW{V6 z{Z<hm*WO-QvmdmV$lmzjh`sqa${6hccTQd!zC=^$B97o}`BrOXdA@AkrtjBj@0QfN zkfqci;;|}yMMF10P&)v+bB8}OJ`MRE_o0`#hp~3tE?^qRSNtG}2>25Nfb>_xRnb-7 zoD@B)cwsN|hnm}~1W7ALzV=+^`1r{9)uAnla=dv$u3g|dN7Wr)xQY%h<&tJBH@@h* z^s!Ic8CqjTKm+Kj@9nA&HFbU^B$))S8mwJE{<%S|tgh~2lu*=d<4RFq{}WzBv8cyB zo??zkJT`d_cFK}vn{KL)p0B0~WseKCa{KJz-EkF`a;v?ej!sHXcjj6AQP9PQT!jIA zB4!w_;4<9M$%SdmPBgGPKVU8u=L>Q(7(8?+p_>|w%R|YjR#&Lvpvi(>r|RHhsBYy* z7dr32IpD2+xbu0h*QC1F*E(^ZH7+Zj^@NEiwIRC<SU`cJiEZERepV+kJNN=^cRa4n z;@aQEGRZ9=#3D~fj)j8j`8NBoMPG9lb2WI$SGf)^%StA)gtfGii4#)!a!qH3hBKf} zzucFUwJA)#p1D#w-A!I_%PzYI$iM?NaCZm7TmR^|fWc;GZWzf^D1eREe3jSx+(&T} zJDzl83tDtZ4t*0IzG8;Y$U9oGcu5S(nt-1$a&|9Cd||Dl7I&L*v$7`lXN^wF4iNLM zOWogBE7DiV03jdzUtZ;xv?3@hbXG;9#^1ibpdTlj@J_UEhg8m0M)`mgJ}G5z*@63U z@Ls#@1PWia%2wzJKKa~5%f=*8`W!y*djZi|wq4>7?_t;p+(Ci7N{fej{vK`~2ff!P zXKdTV6Ze@0Waxd>VH_+()V(xv5c&bSzLTceFEP=PpRG|4INr6GJw%ZVr>Qok);Q2d zrdl4K5DP5Xixwcuj$jVR46bwDR=ck(a>l=<t4I=FdcG2CWhb7XJ}OjL-KeNBN)%|| zAQU3p;;ujatEF@a{BU(XO-WABe!SVV%bb%gx~CAG{(!J6t9@5UPuVf(U429{q;m%d zkZ0H<WfCADB)~zWDQfIXPmfh-?kjn^f4JIdxD~J!DCFeiJ#^NSmr)>W2fPaCct-oS zy9=~=P_8591QvEquj;C4_&;Bh1#^uK+HlYh(7tT^nV%h;Sn~90PHh)9-F$O$yVzLQ z)xd4G8SMw&`5N8Er`AW~?{t-Y(>LHF(2u(I_Q#=R8t2XE(=Qr|+`hn5mk%g&PcN5P z?OAJ6t&{cPN8I>?%mT2cq@0<ZE<dc?%^j!-f?FLt>@fQn1hRdIXL~>Dk|pD(^rsy$ z)J{D5FCtB0GZHL-68-*m<yc2PW=-EfHDb2{*?*!uH)`yEF0zyo??EISKEj{`HEk7S zPxtlxl1>=vhQenwztqQFXeIwNer6`<+Ks!ngdsHJ6Xt)1#*3-~{}6(t0lz<^fV0WU z<9f*w(fEO$>+{3GVPV~`%3oD$^#yK^L>Y@bJbc(s$VAa~)gm!tzn}VUC_A9TVy1fB zk`DCUl@YOD>Dno=V_z^CYZs<By^NE&h*Sk&J9`CbM2l~izpsuN5)a8+#V&68?zNL| z%$Zac<Qst2e48H75f&Vtp5oQBu}W1cYZq`_)nMAxIa9>9YN*ZauDb#>Pt}2@cDCyI zcd<yyRQp}xxN5sRFnPwb%E|m{8$&>}^()+#Ws3KZBns1@Lqf2<x@5R#hRE%&;Vgwa zyRBd6B!^|;;$$+m7Ue$n1g!-QuK+!t6smAZIq^z#%4xEwRuW^+m;##oArZ}ZT#c{8 zWS7?;C~oX|(jzr*$A5b*j(eadh!`_+*W*@?QtC3E3~jrqKgPuxE4RgzINMx-KiLaY zDE~>ORi31vqi9cHA#rWaYxZKe8FaM?pb~gbC-C0!0z>iO&sgGEI2aXKiB#a*ZC=(5 z5j#Su$7=u%539gcIf_ogbVsEyFf5ik0>a>9G%Sd!HxZ7VOX`_;T3TJ<Vbs%$JlPWS zt^BILQ~hw-n37A0S%RcPS}vSRj2X1BqX%f_pp$D73)Q;A#upYxj4=NzwZ$=VCfHHY z)|IaLo=%$mJ(5ag%VK2XesEVrE|#~}XHskyVrxRd$&=nM%^LFPl!!t!_;S4TE0MfY z75032RFtGSU}wVw;uMY!Q!h2MNgYq*MOm?BoP4;Vg6+7ZwadM|&k4hc+1V5%Zvl>o z!&0ez9E?V^{r9hhg~iv&nA$jbw@y@hK24oCQCNlXv9XT|7!tV}x%Rn%9N@%srF<;G zUMcI}xc1i8)?O{~8|V^@$Zi;Jv5PEv#m0{@Z|oI71qW+XjNYjrQDtrgD8!PDKJX## z19mPJqF37yFq0oG#p+4{ejr)lOi0QmneX}%&8Waq^Sswp^@uDnIJ{0cuGGw(Wpl?O z{;0aCnb%)Ybws9z7^1>cAHp9An-mumc9YvCtBsWRDukWFKd13Ak+ICN_-%28@Fa&| z&$;Iqp@R!LoA-Ydol=1r=MNh<qV5}z>N2Me2mTObz1+a-EwvVzzu?xA{dtVN_^R8i zDv4zhbVOz*?RwxD;~!t>A2I&@%|ttHb+}7Eogj7bce|Fwwl?NPiiEDr$vPZI84X;y zpvqhU`_R5q7AUS>E}j;<c2Jus7PGG+8}GAh<xC6-4hu4trsh(P#48T7q|uNTa6zp9 zsLzb%N9c7=QXH7FUm-8lj*OJ_rRgMZ+TzddE>UC-hwWsxh`9i^`h{zG@lH`i*I*9K ze*ZG$>DdvDK{@sAIpho->2DYwLw>=djn23twj{U{2DLF<>uXy4-27sepceE(ma?eu z`h=3-tk<g!fDDRbTlW8eah@|bm$<XDqgAnfc<M0)Dc;+AJTpOc4U8)YKkK~hf<mG3 zfpk(ojgVkxwz>o03&IDNAV#qlAQO<^$eiuw)8<C9Uey>Y%d)gtb?e~vfMPel`A%t! z*b72C!ns+CT7s@Lmdcz2eig67{0IniHO|LvZS**+BBlXN)*g-Ac>c0D0eOU;lvh<e z?#!x0ghz03aWUpJn)o-GzW8<;-~P#Pa$Ie(UDL6@Jz1ar(%5x9Ga3n8k`DMf5dOQw z+7htz`z*!Jhh8~7^hTppfZ0R^<u*6HANc*1tB8@psKaq3xz%<5<u(v&-ET5~Vj_Fv zGYP+Q_sv!HaqY5cd~8x0N{y*ut?A362?!pRSaq>x!+DgIPwH;%R@Cb<Y%n1{{>Hm6 zejtL+_f@38cOt*nt?lUOD0yMw{QSJbFc{QHGk4zF@DQe(goxj8{qor4u&%47cK>kf z^ka8ux?piE^8=^{)cFiuX>25FI}s7<cmIpAcn0oVao}<UceK{lI#1;W5Jn*8N2ILk z2zvvbI<HQpwe5vizz4yugg4i-qj(9pCCbm^^*<@Ue6icM!-oq?%GudT6mb2Vl9(uY zS5}gp&3D!oCH8u~VBn;j$sgd7D*Wy80b>Ky>H(>0UWZFQI5-#@8k*Drd7m$uY&s41 zJP*c@Pup}Vb6KlheErJ)`))g$d<Cq}ipAzbEajXxHV*?w<1C<})ALS3F{8D!PiBhO z0dF}q3d|LC4laoT$72|udRcA_Q;rS*iSp|&SDQEdeAZTv-$W=N5V+a*+;+V8qqCua zhQ7_Z&z=}+DkId^l6q}YyIG7S_Eg%@@ifl2LCZ?jzn|^%k_76=j7`<m(lRnI7;P!+ z)|*SNG8l{@BO)Rb^6d}J67me#oyzMoGJ<ki{HcXsDhlT^huoV{^)l^xR>|pCRgp&V ziZB^%e?Y^1F#IX1Qc?`5FuD7|Uh~mBzW{oAZ?!wp?AR0?8F`)YCH?o?Kk9keNr(}< z*twistd#$x`~peRNgw|_1NxJsBl@uPmv7CBhNVhw6fZ^$`h>=A>^{!og=b}DEeai? zi3IXTfZME&b6&5%D?1T$6>i?|XCtDGg|KR)$t93?vuWY4hz=c|AU%hDYqhnhR*6=L zE7DZ@p^HT!%wJxb$}U8GkeCbr2z%B3A$!oMB0i4O_SQYpWejH-#*6<=6C$q4NUx1R zJ@p?f@?X628m}(j9u3&LKcT)?EWYu*oj2d9v1#GMJjMj{0is=dCQ?86oeaZ!&&h>G z^4-ILrsz9mA$i8=O6`oslhrb1pORD>$$>kLu8@4)amT&TbGd{8+ReXxdDN14hejkW zvoNyFeP7PTlt;@-nN-45-oL4chi(hJ3%`~tNdZ`VGO82q<L(%rZr<E2i0h}J;3t>D zBtI0^SX70WNK}jgua>@|k83DaWV~T`)*t#FK195wzA66Ff#-v9vJe0P12gq0Q1+TL z^6|w5rOFnjGJ$1bA$3R>c~E8`opro$ZDu_kH${oU+SftYmd9X><JzuKzm{Q;E>OB; zHc2b_A~6b?3j%>mNu)Mg1~-n%VX)Tq&Y+53*pu3i*pHB<ADcy4K@RSFvu5S5MQ3)u zK}Y2I8!2d7`92#Ou7bLrR0i%*qw{ejir$|3dR)QK`a6#PkOiLH0dPEi4giltN`_1c zqd0j_&cf2ti?*`O6o6PTyzQUNBS_s=R>Z_qZJjatMkUWl$;dKN5}MERf=AEvFk4B} z$#`0jWDZ{aZ)y~YSjZFO-e}o<|8I&MK|63}nX6YiRhUo;yDw-!*4$WR$$|V)Q6WMG zJtjK3UKgwo4Inq*0Sd1UL0Lq6JK9&&_}%2ZT43|52o;swuG21ENm;XnsP^y5X4lpx zw)#+WW5)LuT3S+PXa+SB5@u%A!7GmK&=)SNZ7)WTzsV5pVik*ImV@C5moBJeMNyUW zkf}G2fZzA}qq@4Jj6N{P6S5{o#lXx=L+<K_!^OsnL|Qp-=kjh(TnGYXD@BGY`Wa(o zZH4*Uf%yLF2kuWT-uJZHJ}I3QqnNmbEGV2PB^4v4!%5kx##$!x3+qU;8i5lq*|F)w zDagrxcL&jtEQ0Fxbr1{k^9hZcokvocFp9n57-sTkLNEcSFULBL{O+fT#w9UxGF>m% zWH=$At4g;imrF5M^*QNQvafE7@Y2%KzGc^pj1F_9YDj_Ek<k=J0760}4WcGjGcz;y zPmWSj1@oVODn~}B0Q6%iaR>tK&ojp&djj93WR=zGvVb{0xjC#(<slv(l9Wtjy7OP8 zw%n-|uu*a$Xnv_IuVQ9oG)>G*%yc=~VPae$#z86j_U#)E&KAR@=Fa}U)X_zKgK1tf z$>NdHUb5lhEultqjyXavO#v3U=!|7kBx@HyfsIK--M<=X(G=dL{_4_2s4dgXjFgci z$m{ZYd3IMz&`8%qd7@=oO|Uas9cWZM!%!bC;=2YpSy1r5BVCXxLn$*i1~>!5t|S~L z7){>y)>~MA^IR)$-)G&vN+B4~4Svi7@&N||u$np!fc>3$*|ndYXjnSZI0}-nN%e}p z|5e{G;)i&qpn$U}HB-5h@Iv6-_3xiZqAX=Oo%E*24<MG0v&_4{kUw&sDHQR`1p$Nn zKRaU?uhcbcvi!tK3QNMAh^G-zxb4w9>bIfp)+49)UhwX6+haA6;p1je2ModF*d5Mu zvWQytcwx@+)tMW5TZt<qpLP!og=7_)*SY+d4T|t#PQvN-5>%p&jBMc%Kt<EcC+m}R zLRtACzfeSN9hfJFYv*hgQ=gb2zJ-5;$2=j1*NvDoLJ_;PFX#PJ{%e7qwJv{D<`*|1 zA@%|c+?Y^}sZ>w+9ZB}6JW1s{g-xY=Ri(;pBU$(q8Z=nv1zE8PvEp#*>Dg(>l(*>I zf$Dr6BvU4YyHniEkGq_(sUa$Jknv(kz|6AHQWjS2kXr{9ACH;7RiQhVPan)WjWaz% zrtr>sTwxhjSx5`6!08T&tf{1LYYAlj+C662mHc<6P`<e#Vac&icl>XI#nVqb<RbHR z0yQ&*;1;TDLw+3rH!m@Dt7#&kdaowap4G=1>>Ur5Y7u>oNo7_M0g>e_gihU=oO7>l zb{Cd3tj@sIM%6826&+&IoCHzlHwxyRWxP3ulB1RLFDV+BsYcvXelSviKg^`z{asBH zW`Go*n6*T9xb{1Ij<nvtHR*S1#J&0X=xHt?hhKaVq2OmH`&w-5oKiOF82z~b$?y_< zN|s-1K{!*)QgN9IR*BB?p-E==?weBKSmb&y^_iS$|CsxiUT4&3l}ZOnk5mZ|VZ}AA zAnw{yDI?9EiLu$z$X`6R95G5DA@g1W&$ECr=raB~2KUZXE{at)0EWI;Qwd^Dha-xB zh^5+-zf{`nEQ;_MkvN5i9i`5kOGqlYf%@7nN;ei*)W<hfpTN)R@KzDY!}+lUux(IE zHaMz(WRfnX9a-x^g)QyF9M3)m$t{xPlw9N1`t#P;AHI_<Ig-NGTN_maE}RXNvm6*Q z1skTtUsu?|P+wnsDyEgI429W_;aPq=74c>C0%jy!|7jmpk?Ub2!^Z03l*5J*<dOad zkQhF$6j|)HA?ig2i=E~UrsM__vm2ur4_KV6<RBDys36H?c#WFQEgz3mXYFGX55C8@ zY8;1bOF@LA%z`@di5s|PcFJg2l#ZR<EEKT6@Ypy<8P`NagyCKhNXXYg4bh4rY!a{g z3|SuS5;a6u@lgX{3UUIbJ-Q5f4Mla|%G4D0xdoFkxlg!P>fghUz;9f_+*rMCS#Ug= z7x}JChi)KT+*$HV47yZcY5}s%MEC)kc#Q~I;@vx3=KPpi_q#`CU%5);`S1PVsu=$} zz|IuXeLLLy*0<a|EchuX$tjDq>Pg;|PC5oqmi9GSmU4j5uc^67|GOodP-ol;XQ{hS z+=QXK$XrgK8|d^U4X=TZwjET;Qy5T<#Q=GXKJ{_Dsc;f`s%bg!+QOA2q|>6#a<=fY z^qTy1uWesZw1!)=dU)*Np);4rT<Y(oQAA%fnI@<+#!2fen1u+BN@((%xOWnCz6y@A zcgk=l3p(@Lib*Te`&1lZju^xf?q1Hq<2895NQL}XYUFZ<wvOgjma{N8b`Ou>nMbR` z){))-exm`>ZFtg|!)vJe5Csq$iR_2Z#a=EIf;i1R4wct4?Wp)&wTWNZ(%Iiip8&fp z+VEDzUEjWM#2N{I69xcQo6h{C>BnM^7qd0ZH5Im%IIdU(4jwC5KK@UTB@#=`o&+WH zvm^b7%jY7iqMVoO<qZ3EY*(F-%}}k1={Lcvh^}~xA5%k#(^H-&ophW~!(Y2AwLloD z5bN%FEV$Xw@Ax3E0JPNEvtWxiisSp%3Zd}(8@FP;Va#8uI<_c#Ex5)F@`dik7z0fB zG5=(#_&;f~{DecWCKZQMhzbuacPhtoP5ihUte{&55o(HY7HkK2Amd{&(Odrsk4=il zP)sOJF0M>u;oLB-zgeu>IMaJQprp8hcn%i9H_!yG+kD|Jp)KTPoD1tkkgYU|W;05~ zgyT>>ycrqO8zhV_>J?IGiuvu(p{@hJ$&J6RL#3gMNk0HQKjBtdFwkfHTF05?A*94j zO+(F+%v&&BI(xrDjxmmr<NJDH5c6a}wCAMO+s(5Xp!4GUSh=F2lM`~d8a}|>@4_f< ziWP>X*p66SMI6wB&;4FdVoW4eb5SQH^Fga(-u6R!1-+po4`c0;9dBCm8QAAJGtGKJ zJ9TW232*Rh1?1fQy|U+9tGS*uzn*(}U910egC|b)MnT}fv5-m!bMi2eDQWHHbn`$5 zzRW!&5c$>LAT9yMzlu(ME9u?dgT5m5gFYJC>nQpJ99qCR?0>Y9|J~<&1J^iPGN7}U zw_Nc6moZ*EZ!^&G@$EH2CN#UV8hqowp`?9@nQTuZG+lMN#B|bm>TS}2+Yvr!ld=A_ z|B!^(aho-;aW;F$?@74zf6WW{sYVEmq*2Nx-E2P;!U+P98Er_v|6f7+f5h)AmM>Bm W9G(85(c8@c7<p;skJXZ<KmH4vU+PT& literal 8267 zcmch6RZv`Aux_y65JCtV^e4dw2@FAl6D$OGcXyZI8VJGNZD4SBCpZCyA-FTR4=~uB z|2~|$x9XmA?$deLySjRJ?Y-7s-Tn2~9i^-!gO5Xj^Wwz|{4cVSDlcB30Fm+>7Ao@Y zv3L21R5)5oNGMyIn!b3!l;EBqD32!gCahaBq<{z)LyApBvR3vVrpV%<zW1_`I9(Y> z84*8+>)OSF!A7QZT9JHRIL|>%!Qfz8rr^l1?jB7B<IY6%*Z-uO!vnI?UDH3Ioyba0 z>452e@7nS3QNr2Tt8W)q&o}eqzah5T%oSt`i8A7HW%NKI=<3zG_Y`rH*P+EB75ZM{ zKm}bdIsQ6p%4p9p-)lEN{aZ<XqfwMF$K)#YTSaG3pJDVw*aBXQ`d33xT%*AJtc8Y` z<U;Pmo2vuwImM2s<;{*cl=Kj-fi%Wg!XK!VW;NJy6JIQT_~+L-#&vkP-BG^b8cd*P zU;SQ&v7hlX<Bc$#VR*slV_=Ski(lo;3gyeGiY-3^u_citIMv?GoU@$e^aU!kHA58e zNiiU%OLO*Nc)Alm709b7_`TvCERb*0qJypRx7&xrGiMb2v1wPrcNNK%pu;TD-o6x} zKkulhduxtP4errygsomm-yEtUPXOCVR@?Q(3qs?6FO)LTQtKBlsDr;qim7=S9Is%0 zCboRWJo|~>x9W?9_4}0+krdI*D%j(1myW;g{bJi9qPkJT{<vY$cx`cUUg=)PHrrhj z>|3LTPW0CoK=;q9-?z`dLKuEAV5apjJctx~d7pTu;*Q+p?ER8A$x~+H$D#{%{)_WU zd<9Q6`qe&XF;)V>|F3Xshk<oXUz}WQ$C#b3%oK|;q_L}E7dDQ3iLlYszA#d4Y;CcB zd_b~JzHWEgr?Es4=S6gY!&{KCWR0D=2`Jqwu7xy(`!kE{mzH(FKt-#|1052=AP!ZK za#vv%VlaVqdm6R7obPt{g%kfvy{aDNJab0z+L~{ROv!!EKR}E-qmW)u5;4_*39b!f z)M5wnhm>SAFtbW0f045>X7m}tr_e~-*c)Afp{~2?Ck#qfW!-4dv(o`IQv1lmGApfC zQ&L)S8dG+ciuojzQXAhiEmx&bt~T^eEPY5EOZp+lP5!9+=OfB99}x7U{yUR0nRI_B zWq)_j{H%IWZat8P2baH+V!`nYQ(ko1<;K73zTWq*=cU|*D-L&KlJILUA&%vMCKeCW zpxW}<sO%qGq4|mHoBqb?HU>7$#cIhCJAvOZpRK<O>dw#x(F75>D%%b=s-(Y>KJ&}r z0LG$=(%d<pXOaE|QO{@CSJx-IsVGi7-b0cZcNlBF-k$Bmli7+aJzDD+EVd0EcItJK z<Nnsv(Uzl04QO_3hi`iPl0Dcctuk=AhVLZM73MKAh`J7ZPY8d2i~MfCYDu!qJCZeS zUeO3ro3}Ey`EyCXXc{@tck2rzvlNTZ{FU9|v9TxvEbvhW5bcrfsmwaX>S*idcN*xY zmb`Il)e9?__=b*7#vs=+n!z&U(PC+w7efRoP*RckS`qiWSW%%m`t8_lDT6V&@$Uq^ zxLR|9$~0P>)HTjfryb*X8(lG0K0$91y`+Ewvv^Vee+a3<)F__@Kh5vXX9vh&RM4&& zk-KaB63<C{J*Bbw1#^;Sp~jzGH+klM3^+vzj0b~A|Cr0^Dut<L_e|$&hAv9X(3V%0 zm!;12heS<nwF`QmwcuIuyQ^JgTLpd%w8gb;67L-27>(;9zP>85oTF5AomDar-rd>| z<Z;o^#r60d!737T51X*B*iS*}LfgTSiZsMp_ODRYu-1kfZ>Me(65uR7ii`;=<t%a1 zu<$NDKRy+d-Bw%;W>W@IlfV9Czmo)f+x1Dr?m^eEd&h3C<AB`X-tT5+Cz8|zdn0ta z$C3R5nR}cX3wsq4HxJj1+xdmdc~fKEmXkmY=__bVBKK*HdIA$WfS3LsZebv}tG>Fa zT$kIES>`%I7cuA3=6qVd7O%&@ZFksl4V|zDAZpkq*kVR;Dr{{V=6dwXg5-u;D@ehL z3z-U!l6X|u?6#!UxYvaC>P_eE_z?hV3!UCVxm15<{7umGwPq^>I4AM6e?QeXC+_Rd zs*>K2Uz-31b^fYPJT+&u&9Ntx7)B96TSz%ci|zQ>E%L}V0am$G!9)<4u|h2dXm`F8 zaed*c>5SuxE9@UKjQk`&u~@Ucg0^>)s-9$}-z%XlWb5n#UkN)EYPOwqV08_r5|=b~ z(0VpjF8`ETc%;(ufH~#=j>jw3LR&h(N48rm<2ZKP<x}cXde?#IR}Sb=Tld43yngm$ zMHSD|uw1?mCZ4YPFc~?8*o@7gBzTdIngl{(?QMNim$>1XT(du(HkvUyx17}RtK)V~ z_mkA8@=we^e*9W!=So-D8<wSDJTE^_buj42aRL{q+{sR`j7PT-OOf^@Y&3?ZY-r?Z z8hzLNT#*c8D2O7DIukr`z<W^KcwduT-$EVez1QnlzW**Tz@7PoidWcloa(lp`cq)p zv6Q>%STy*_0A;L+%{;5!Xse!+)P`)2JL7iL>$)8~)`0s0-kEtC_&hm{5rR90lfUD3 zm9AZ_NYiBn3--(jF~x~HI5ri#e26m;5rB&pBIBBQHu|p^1zq9+yH7NeOfz%ws%Eht zwKyGggs4zJmO0jCj72_r6nEoAvO{%?p@F{c@%|C{Itgrd{iSEDGFy=a1OiQ`$Rs!b zw#2*U2IM@2hX?12^&{G@A$r~p&Q;YBr?6?RF6sc!8Dc4SdaYz-HMA`=5N-EAgyDY= zV$gjs?|d<5NJ9_@A_cVUKwcB0?RwRmOyI^Bn3_8`402U%jhkin4p@!2N`#77R`_eZ z64%6R<~0OcL%Mb=fT$g4f^WV_r+OIRV`HtH&6F&xIkXD2^z|5T#ktb})?6O<(wvQn zH5~Mn;xAs$5>Fnt+#WnkcE`vHmmy-1J7zGr_N>}R!}FwbpZ+3FXl-j<DDY7|=izu4 z_L4l@I?dM791~;CD{Xk|Z@G<@podmSYt$zN{ssO8t;HnuWS>?a!9@X*B$8Y2TgW^_ zT#t9{71{PXbWd*=3zfXD&+A>i3akNIu9>+94cyH>u73oM2ebJCkBP7D<^S5auOAOm z<}HQhIcV30*#t%Q5e+0v#akPm9d<4S@E1|xl96Li2Ks*S<uZpx8Iq2Bi7d!?pR#4t z!YYIfKYQBPnc<O=PI~23^feLK!^{o<1ym~~4Y+6Ce*AfL9~;L36Tr7=J-NCZiRIm3 zs!g2yf)e~YY5U3&Ug98ZVRlY#ho3C*M#5P_d4^$0eM%@LtqR{x*P2BiteOsSsD7ks zl#2l5Q7GR#)w!GK79<L-Ao^sPc$)iZ;D4~4lo3n5-4e=yA_yL;;J(v^B3OTJo#Y#~ zj7R!wbpAt!)R!T7X#<Ddxeq_ues8&qfkxL?L#;J^HNi1WsRox{kQw#*Q4ViH%gB!H z)!dgPG@S)fzK?v<r=B6!g}Y8o&j@py)4}q@Nbq>_N(s1_eHk5=j`V72yhtlwYvJHp ze}``z59*G~KkzTGbErhp?W2@@JV)N&b{ajR0Z{h|(cSxMzVOoTTy~yO4)w%NeP}eX zFd+ze*95{uo?l;YU$8aq9OXt!QlU?UJ|!-ZOe!S^s%c_0uFYCiEb>`W4G<gwfS~AT zYo)5a0fKx)%aA$0rnw2KM`gm6%qwTEGITT1`#Z&^y0WT_Bqqx)yWE4YRK%0#RHsaO zF+<)Rg$yQ55FN_#M*1ERQ&yVk=_4lXY+jq%PN;j#&@;L$P(iDDS1zQopT!mb=(0Wh z214uzX@ry(PSvn0((A*EKl*pPM)U1rF(#qn5+Cl{qZC*aP#p<EacULw)-ZlMF#dBY zLXYb`4uw6N-KMI;r$qs{Gu)0By4WW`_sC~P76*zzC&(8)K#kK>txJ4FGz3T~?Qvi5 z6eJdezr+vOFB&frB#*jk#XnmRAoYbi`{%^)aQUe?dndG(-)$4Adb*9|X;#0T(0LeY zg2o#lY}a-}co-}-)KUR!7tzR27Q71MNjKc0%Q8>_#cP_ZznYuqRjhwkg(WSPfksDG z-52i@QyoDQ@x{xD{{cQ1$su$7&01O-b%Drga)zD8GdDIkwxYTE_fDNW5YR*WBgY-~ z(3=n<y?+|{rpK$RrGV_c_=C8|(PK2Y!}p<KgLGo3e(hst+SPeo{Qi)!SU*QVU|84f zL#)(UVOLU2=hzBb{LG|mDuY31*X!BA^`@Sm*wS+TEB<a>!^Y@nI%L*RC{#jn|51E- zrR4%NBe);c{`K+H?aKPt7Dn||(=kEoYCdo+Z<7294r>zwP+9q%&E5y<^TT#wlNw7E z`*i@w4J(&EHuY!Dd3R?g(<9SsGo6734R&KO<FkIPW$VCf!MvwRF(u=95O}O?KkRUC zZ1JgsGCM{wnnKiV6Atu5_r|P+z-`(TtiNuaz)s=>z5I2q$3ZzhE*+EmE(0;IW-$LP zRu03HY`l5Xr+gc7T@Uap+3XiHc1GOdJ{Dl35B*!(2o$@tF8Ab3u<kf}Bk>g68j0Sd zLD<nH72-SJKO)@D8RN`>YGG5oeYx~_O`OG{RDR)WLcyqGP_%K07iL2DGFgjHqe;xZ zqxwa-?2|!Irl<W-9yd2Hmuro!K<UKfqSC!*Mq|NPVyMpK*5Ik$Vbofyg}uW&5vPGR zDM#DZ`+uuKIS=lDy$k_}XH~ku;-~m`+&MPna{x01m=+jU_;V_=6zp5+Q?{+orqv-o zi&;{;DyG5S;S3Q#ypu}uV@IG@1pR1lJp~qep?|H`^7M31e)wNP8;H1J(R=DA6JHyj zn&^Y*x&rPd5CQi(8FsADvJj%p_rB%M<6{98)%mZN^ek_16crWm@$qT$fuHHwDJUsV zq00bv_Um&v7vITv3|E>;-Dk-kv9ajE1VSj0)Y#%An$StV&*&%M5mYpNnABg;bRp)D z4E?vmuRV4j6qvP*tU}OMsH29r+<lg#39&+DBbcQk-O^P4UqWf>rB^wRh;H-8DI*)Z zLz*~oaE^m!nW2@wWUl9^_s?DFZ(zd8TJQm;qe6wfo%Z+ggpKzNs{xG_pmLJGOFVvj zq6a7!F>?6oh7|KktJI1gSoRr<N2d^eXFPWJa~4+f3>lTRJMuK&2)9l)WihKIoRI$U zpHL_zK=lBvIMbWxz)AfUw>y__-)a@!*Vi}ewjPlX#uEgd5F8k!&Ey=#citBinC?#a zkI&Akvb`-WDdCelCmx{@PMe{|6_}Avtsc+3zlD8EP?I<M-VK*}m44RJFwD)#$q57k zn3$%sFi@UcpMx}0s(l`!N^=b#HlIQw_(Ac2EMCji+<Ss)^O2Do@d$#ciHVSXSlYse zTyGA3v)?UIs$Hw+*$Rsn!h;P(?IZFj9JujhyiRA+7%>LFzp-Pnu(Gasxvta@C5WND zoJzkh+6|T!L<R}`O7*K=r-%Q2oFgu7%-qtB%V4K3GXS7}aY?wwuG8SOM<~j;`epO- zj`%~Ys4JYN?dWzyRU#a3e|C6F!|!2iOhf2yVENMWdA07YP&n}D(HqL~!*)wjN=hb{ ztjqGIig=g+$j4Wu+nr!z*lA%eB{dl2ADH!cKM=f^1JQd`t4#YVDLJ_|*-gR21O6d5 zpv&nzX6@*<tF0}gcDFb+^|{Lupiop)A{|Gbo0>{E`{Tz<lxj)t!~I8v?z5ShuT25c z(C4eMs*za>?y6{ygVWh**HQJgK(rNrT?ibpO3TO?1`Z{sPK7(=<mA|Dbf;vbzz-eU z*ZdBTZ-AY)j*gheBR48l`Z1%UBODwo;W;UYSEB&|0si-hth>GC%{@}dVM&RB_py|? z$Jp4O4yzrFj?ZH02TM75xn><430i*Z>-m&Ghn?-<4g>-b9{w`ubB)d5MdRUoX=&;6 z;orMLdKn6`bwLh+mogolr~{5$OOD7q6A@1p7DLKm;A1&^FOAwiY%#HXHTg2I(K6XW zA=70y)@9mvb6}0fQlU_UYGL|k_f;`t1mTnS_SuuE48fEC1#CZYSVuohl0N-@V0Jhz zmx|YBTaa?~2^OMwRr#sjGDpyrrDyHA5n`LC>BzcW#=ejW*TR8hYV611B0gG@mfox1 z7$Y7A<%x&liwTLw#>V!GOB8Z$uYqf;(cS+0qmbuE1Oo)?ot=#iLS9ES^<A9Z>@1H- zVb#JSOLw@}bCAS=tG-^AVW9iYLuoFBu)}8YnCAOf8t9PCE40ocwO;r;$#*t(mgJ_U zf@5Tq?z%$@Wq;}4;J9CNIDL`mPkmky;im6fSyALj?Jc`jGMO1A88b?0Om_EBatJ93 za{T>ldfEj7vG_2vvQ|V!60P~&`ztHc#-@}HF#_0py#LAp08`pLma@e^NC5`n#+OIh zJdo<i$@#Z%tn+5%t|&7TGn+4mb79)LqDy@C2WFNZN<#-ETVUHTETG@xkfLg-LNLnN zbs4O1SgGUYM4CEY9_aFu0=f3f9yCI!#V?@tdIG$=a1IWR;jdXt?5rWyKp<386O2Xq z8mIoP5b(L8+Oy+qM#YpkafJ8-`b+dO0KX|>?pe3~t#qH;7&6Xj0&4c#PXj|8-kJ!R z{ayze1cD(DZ(rZe`t#!-r(Kw$1_lPt23JClRE_}wPkmB=o}UX)sONoPHx|JO;ViVU zz|Y^G_9r=cIw!mJ&oghKwuD*YZ)=56>|1*!v!LG6Dujw;#v!yio{TFP9E~F48>&ni zM1-!3&4UCw{i4||jsKgS{|8O~Zv|~UJUk?g(<Sk_`fXOH)ICO_D)*Lwf#E?bkNOMS z+sw4I8g;JOF-zV&r4T_CrZ+B0G*}kDcQ!>DKKzs`e)Z?#!o}9swn@hV2@_{`ez>@} zEvn^}bpdLx{Sz|0Zexzi#oi%df`^AkQxm$mY4X1<H$N-0v$OvNDFcE{S^i(J@LwRt z|Ky`#OI8?XnD||VwT^4uU4ahIgNoA}M^i(?xK-Qqyi}ODz0xcRGZ_HnI2l61u4!Xr zWMpP$#m2c=Gs4Qm#>{SOWffYyJ;$#3RKh1im)fiVY-B2%&|unD1W!B*)zk!Y*xEQ~ zvqMlbq0ox2&!3<9nk%)BCT@$i)e>acBEa7az`tB{u5+{66dIK$)U;<E)k)N8ZL7S_ zzOE>}eY?vL5uunX6a@b1x_Kq4S-2V6r_<EO0eL!xVunmU{_*AWVw;D^dkN*h^L9z! zK%ylLa;S$;gHpKt%SfUeKfKr0)wUtDSq3ld>?9Ly7^kGX)fsL3>@Bs^%kW`&MXy10 zny%nXGh5KNtuq^(Qdb3?Jce=z3NDR}#UQ;f&Wjw`Um`XYlaL63*Zr!yaEM~!1oGgM zPm40J%J^!#$oUMMoy0SJpA&=UCC(YV#6Xu$nmsv&5-TwCV1csilm09$EU*(5e^zFE z9R<&5Yx9whLE2+b!z#D5m}of*lHhjTIYx3s^zTaE(9IbCCxY6k!?N6W8g>k8^7N=L zBN@l$J0zA<hVypNe4nW8hvEuBk0->RQbYTdrmY+Qh`d^9t@fwRuM2OF`P|*zE7lg> zuC}e4#O+Tsp_P87Xu2fKZh_RN4Ebd(1AlsRhPVE(bC*|^6%DLRsE?^+)4qj%p`h4y zX_eKpw6N*ZSS_}ZDp(1ma`SLWyI;W5wY9C)%1W4U;U>o3+$3J_iiwKJ<ua#FU1DuZ zH*jXqW#(WGN-8Na|82$%!l2K{`lX|+oMxz8=KnX2g@f6>rrEWoIgHi3;@GBCDc94} zHJ!&5lo-;PPq#KY%Kg#o#gh1O0W`7S)WlIS{iBkRnHzvN1l$w>`{7?6(&-O*JP>f! zm(ywM&IL3q%PSH`4VILYa40F+$$P#K(u$EE3l2fu#<|OGXZG(D!pFla)Bd>{TS*mF z)H~BRKCqBxP$bQy(3Xj6s>BeKkXg?GEFo!$9?x1X2C&T;XlNP+Zr|?DT`4SFL~i77 z$;1Z_WpxC1$7LgzzU6{19Y-c$KjRg5Fad-XI(oIV4=qO;az01V28tfro@D)+xu}aC zUH|<2>(>lyfl>@b5n5-tMn_D%)z*fnyO1O6#-n<LrHiqoV6Xl?_p6S@+vUMHIQ;8G zroGJ!ciG)uUvj9Rr=jlCYxi><H!7-}N-czA_~D62Cs#4&^zq&yntyqV65d_t+cZvI zUhIpTy>s269zsGmhM&mPOdnN=_siwz(R;WD9|(Rw!n_|M5ex>i<-b4C2(B*_mT@<6 zQahUX9?;dlqPG!AB!Hj7>V;Ljv$MNvG1p{WdmoR+*QDVHHbB|hsP29qoSf(QhhO%e z*HdYHf7;_QsaEWt{Cu5^UW5`yU*g7uM-bk5&3`1zv@*0W6^j&?2;Qg`aQmG{*zAWL z^wC}<E;260@a_VaLn-bj#D49lp|**Fiw$qF-o?eZB1KUTPjF)GhFZw!mj6f+MiL$o zM*Vzm=PkwP{^aC#G{IF!PgdPVc|Ak~h`<5m#z#eAFHfAI5evjK$v}(+J_#t#PEHbV zj+c~NUc?A3>(dr+;uF5tw3GpL*0c=tjd#SiQ@Wu}_aYEpJXIPW%v3&IQ){y4ZH<nw zW(nJaTntUEiA3y_#W(e>BB;lQ$LiapwY1&W7H8_pijqJPA~squ+O+9A!(XToH?=?q z2@&JP`Ju(JSq7zJ>sB$eVC45uvTX*=YL>7)%RK}sDQzm&s|6hFwvPwe4-P*9f%xRF z(DjjA_I>HaDCp-?5LMsRP*)PFSl9*!B=zO(-Tp%mW3n783rKEkl&9XMoPpOX>9eD? zm6evJ<{r#QMw5b^d>+cWwt{+(3tO98BA>5d(9n3xi5iLmMtHR+M}t_(6%glpOXW9V z{l*>b6+AMZS`_&`E*g@HF_xplhooDI)r60n7j=@AdV-jaD7qH95*>nwhG7XCa}5Z} zD}`9a^?9yF%Kn#VaTy8y5yy9zuVQ~9qmF=v0(2?U*MHc_yi*oTF+CNZVN`L<Nv6`b zg$AE>!=*awF$P6svZAZsofAyJfO%qq=n)RpmcFKo{Z1~(=0{x{^*dmIf|)KaCv|wl zeK>CgtlxTidiv}>7t>I`X<)V=pCh35(bp%%CtYM<U?oe$`(f_OR$0v1jkoWt1=;r& z5W%{wt&|%hp6=0c<|>LNtBo~)jU`AB<7v`qKoN+5UvFNtr2lQL1v>U0Z~iPzjq|b= zB549mGBGok(lEXG@Bz~+sVB6sP{Kfru2F_`J~~={tQ^%+CZg^lI)j2v#yvJ8IlH1( zBRfb^G5aHY_gIj$O0R+lCqFloVY{p)^MLmZ*38Kx?0-_w@C|3h+QUJ_E=M8kAgJE; z(&f_qq0&p&=*6!zH%Dbz&J`23SLm3C{)B(r&xgR0{ODm(u}4|9f1DE-G7ws1lX`6w z_)CPqvxN~J<ZrgA5~BB8c<Pr~(aScb1%GN@x<2Sq3p8S4t_0g(JAat=FD^3w%(kMJ z_0KOv8fEjXP*-_5c4&}~B35`i9oqexob0AEzER-m$!^97+!~J%jo>?}(B;6+;UCe5 zI++<-E*{=%-!s{?u^o;FsH#j{u8-pw-$H7yljq6Nz7w9P<K)$8z_Piu@y71%(Gs*C ze0@0o<5tkagKu(T_%3O>m(KU*nb&vGER9;=pY1DA(tP>Q{99F5%X=@Ny_O+hDtdI( zJ(G`;oPq!!KV8T;c6YSD|K+3m!}iq(F|Q?&SkR)fx_)8jy3aWNF$My@*UPYo+jYMm zxQN@f5dEzC{1d}INbELh_Svebzv^tvF<{!xkx`h(cMDV3*s#2_ayC<DWOUSo)fhRF zJAfc@NOI;3TB}Mt98V8dc&~ue6__|ui+HdI**R^W8QV3kyi|?$3BLO(GPxk~(QR58 z5>sp+g+&;G1k8VE8)9Uz{`?;U#qobz`o{_s5)u-fIn+h^U|BY!`*8<kil)d{_Ek(} zt}bgZ5^SZx$y4xE^t7IWA7Ymzp<D}dNlUY>?QU-y<z^>B*~N;xK7<Esbg>6LbwLX@ z%M#PuNUw!i86xV1zM^3$OB*RKwLbb3+&jAVo$A@HQawyIzCC`PtfPL&w#|Ox`>;~_ zhHjv2mU$4_=lDNnPXBE&`#)i<FUFBdNk)dOijl*xIg*0`_xJDL+o}IMP~A^eV5tCu z!EJ`-=H`PMW8<Imm1`hwX}bdC)y3IAj@eA@{v}F<4oIT3q>Us>tKwe|J;X0ruFkFY z&yDhMJDDWaXkXPpxM$_%h{Od#84Ui*;`rt{Pg<=-o~9K8Iih~?MM_DsO57;;e*we= BL(BjG From 50aedc109401cc8707fea87489a98d717c7c515c Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Wed, 16 Dec 2020 17:52:43 +0100 Subject: [PATCH 0158/1708] fixed an issue with clickable types and correspondents on the docment table list --- .../app/components/document-list/document-list.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index 3bb7e00c7..5c09f6c13 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -93,7 +93,7 @@ </td> <td class="d-none d-md-table-cell"> <ng-container *ngIf="d.correspondent"> - <a [routerLink]="" (click)="clickCorrespondent(d.correspondent.id)" title="Filter by correspondent">{{(d.correspondent$ | async)?.name}}</a> + <a [routerLink]="" (click)="clickCorrespondent(d.correspondent)" title="Filter by correspondent">{{(d.correspondent$ | async)?.name}}</a> </ng-container> </td> <td> @@ -102,7 +102,7 @@ </td> <td class="d-none d-xl-table-cell"> <ng-container *ngIf="d.document_type"> - <a [routerLink]="" (click)="clickDocumentType(d.document_type.id)" title="Filter by document type">{{(d.document_type$ | async)?.name}}</a> + <a [routerLink]="" (click)="clickDocumentType(d.document_type)" title="Filter by document type">{{(d.document_type$ | async)?.name}}</a> </ng-container> </td> <td> From e4ec52ed29d5c3e5aa6cf3dde8e9119a4b56eae0 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Wed, 16 Dec 2020 18:16:14 +0100 Subject: [PATCH 0159/1708] default saved view names --- .../document-list/document-list.component.ts | 2 ++ .../save-view-config-dialog.component.ts | 15 +++++++++++++- .../filter-editor/filter-editor.component.ts | 20 +++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index 4b711f9dc..25d92e9db 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { FILTER_CORRESPONDENT } from 'src/app/data/filter-rule-type'; import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'; import { DocumentListViewService } from 'src/app/services/document-list-view.service'; import { DOCUMENT_SORT_FIELDS } from 'src/app/services/rest/document.service'; @@ -83,6 +84,7 @@ export class DocumentListComponent implements OnInit { saveViewConfigAs() { let modal = this.modalService.open(SaveViewConfigDialogComponent, {backdrop: 'static'}) + modal.componentInstance.defaultName = this.filterEditor.generateFilterName() modal.componentInstance.saveClicked.subscribe(formValue => { let savedView = { name: formValue.name, diff --git a/src-ui/src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.ts b/src-ui/src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.ts index 284be49f6..8f0eb26f2 100644 --- a/src-ui/src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.ts +++ b/src-ui/src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; @@ -14,6 +14,19 @@ export class SaveViewConfigDialogComponent implements OnInit { @Output() public saveClicked = new EventEmitter() + _defaultName = "" + + get defaultName() { + return this._defaultName + } + + @Input() + set defaultName(value: string) { + this._defaultName = value + this.saveViewConfigForm.patchValue({name: value}) + } + + saveViewConfigForm = new FormGroup({ name: new FormControl(''), showInSideBar: new FormControl(false), diff --git a/src-ui/src/app/components/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/filter-editor/filter-editor.component.ts index a11f0736a..f762c6138 100644 --- a/src-ui/src/app/components/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/filter-editor/filter-editor.component.ts @@ -19,6 +19,26 @@ import { DateSelection } from './filter-dropdown-date/filter-dropdown-date.compo }) 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, From 69c04a209acad8493c69b7450e9d9e06582aae18 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Wed, 16 Dec 2020 18:18:01 +0100 Subject: [PATCH 0160/1708] changelog --- docs/changelog.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4c72b45bf..330dd4cc9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -23,14 +23,18 @@ paperless-ng 0.9.7 This means that you can access your views on multiple devices and have separate views for different users. You will have to recreate your views. + * The GitHub and documentation links now open in new tabs/windows. Thanks to `rYR79435`_. + + * Paperless now generates default saved view names when saving views with certain filter rules. + + * Added a small version indicator to the front end. + * Other additions and changes - * The GitHub and documentation links now open in new tabs/windows. Thanks to `rYR79435`_. * The new filename format field ``{tag_list}`` inserts a list of tags into the filename, separated by comma. * The ``document_retagger`` no longer removes inbox tags or tags without matching rules. * The new configuration option ``PAPERLESS_COOKIE_PREFIX`` allows you to run multiple instances of paperless on different ports. This option enables you to be logged in into multiple instances by specifying different cookie names for each instance. - * Added a small version indicator to the front end. * Fixes From ecfae9dadd7a592df2c1a98ac3e1f3f6f03d12cd Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Wed, 16 Dec 2020 18:40:19 +0100 Subject: [PATCH 0161/1708] fixed some issues with the ordering, test cases and migrations. --- src/documents/checks.py | 3 +- .../migrations/1008_auto_20201216_1736.py | 34 +++++++++++++++++++ src/documents/models.py | 6 +--- src/documents/tests/test_api.py | 9 ++--- 4 files changed, 40 insertions(+), 12 deletions(-) create mode 100644 src/documents/migrations/1008_auto_20201216_1736.py diff --git a/src/documents/checks.py b/src/documents/checks.py index 3e3ddb1fb..b6da5bfc9 100644 --- a/src/documents/checks.py +++ b/src/documents/checks.py @@ -2,6 +2,7 @@ import textwrap from django.conf import settings from django.core.checks import Error, register +from django.core.exceptions import FieldError from django.db.utils import OperationalError, ProgrammingError from documents.signals import document_consumer_declaration @@ -16,7 +17,7 @@ def changed_password_check(app_configs, **kwargs): try: encrypted_doc = Document.objects.filter( storage_type=Document.STORAGE_TYPE_GPG).first() - except (OperationalError, ProgrammingError): + except (OperationalError, ProgrammingError, FieldError): return [] # No documents table yet if encrypted_doc: diff --git a/src/documents/migrations/1008_auto_20201216_1736.py b/src/documents/migrations/1008_auto_20201216_1736.py new file mode 100644 index 000000000..d94f4767f --- /dev/null +++ b/src/documents/migrations/1008_auto_20201216_1736.py @@ -0,0 +1,34 @@ +# Generated by Django 3.1.4 on 2020-12-16 17:36 + +from django.db import migrations +import django.db.models.functions.text + + +class Migration(migrations.Migration): + + dependencies = [ + ('documents', '1007_savedview_savedviewfilterrule'), + ] + + operations = [ + migrations.AlterModelOptions( + name='correspondent', + options={'ordering': (django.db.models.functions.text.Lower('name'),)}, + ), + migrations.AlterModelOptions( + name='document', + options={'ordering': ('-created',)}, + ), + migrations.AlterModelOptions( + name='documenttype', + options={'ordering': (django.db.models.functions.text.Lower('name'),)}, + ), + migrations.AlterModelOptions( + name='savedview', + options={'ordering': (django.db.models.functions.text.Lower('name'),)}, + ), + migrations.AlterModelOptions( + name='tag', + options={'ordering': (django.db.models.functions.text.Lower('name'),)}, + ), + ] diff --git a/src/documents/models.py b/src/documents/models.py index 245bba6e9..02a293eeb 100755 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -13,7 +13,6 @@ from django.contrib.auth.models import User from django.db import models from django.db.models.functions import Lower from django.utils import timezone -from django.utils.text import slugify from documents.file_handling import archive_name_from_filename from documents.parsers import get_default_file_extension @@ -80,9 +79,6 @@ class Correspondent(MatchingModel): # better safe than sorry. SAFE_REGEX = re.compile(r"^[\w\- ,.']+$") - class Meta: - ordering = ("name",) - class Tag(MatchingModel): @@ -206,7 +202,7 @@ class Document(models.Model): ) class Meta: - ordering = ("correspondent", "title") + ordering = ("-created",) def __str__(self): created = datetime.date.isoformat(self.created) diff --git a/src/documents/tests/test_api.py b/src/documents/tests/test_api.py index e0a64664f..49dddee87 100644 --- a/src/documents/tests/test_api.py +++ b/src/documents/tests/test_api.py @@ -169,15 +169,13 @@ class TestDocumentApi(DirectoriesMixin, APITestCase): self.assertEqual(response.status_code, 200) results = response.data['results'] self.assertEqual(len(results), 2) - self.assertEqual(results[0]['id'], doc2.id) - self.assertEqual(results[1]['id'], doc3.id) + self.assertCountEqual([results[0]['id'], results[1]['id']], [doc2.id, doc3.id]) response = self.client.get("/api/documents/?tags__id__in={},{}".format(tag_inbox.id, tag_3.id)) self.assertEqual(response.status_code, 200) results = response.data['results'] self.assertEqual(len(results), 2) - self.assertEqual(results[0]['id'], doc1.id) - self.assertEqual(results[1]['id'], doc3.id) + self.assertCountEqual([results[0]['id'], results[1]['id']], [doc1.id, doc3.id]) response = self.client.get("/api/documents/?tags__id__all={},{}".format(tag_2.id, tag_3.id)) self.assertEqual(response.status_code, 200) @@ -199,8 +197,7 @@ class TestDocumentApi(DirectoriesMixin, APITestCase): self.assertEqual(response.status_code, 200) results = response.data['results'] self.assertEqual(len(results), 2) - self.assertEqual(results[0]['id'], doc1.id) - self.assertEqual(results[1]['id'], doc2.id) + self.assertCountEqual([results[0]['id'], results[1]['id']], [doc1.id, doc2.id]) response = self.client.get("/api/documents/?tags__id__none={},{}".format(tag_3.id, tag_2.id)) self.assertEqual(response.status_code, 200) From ece94379d899c9be3e55e8f3462864deb0a5271e Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Wed, 16 Dec 2020 19:35:21 +0100 Subject: [PATCH 0162/1708] fixes #143 --- .../components/manage/generic-list/generic-list.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-ui/src/app/components/manage/generic-list/generic-list.component.ts b/src-ui/src/app/components/manage/generic-list/generic-list.component.ts index 76a92e4e9..783c22b36 100644 --- a/src-ui/src/app/components/manage/generic-list/generic-list.component.ts +++ b/src-ui/src/app/components/manage/generic-list/generic-list.component.ts @@ -95,7 +95,7 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On activeModal.componentInstance.message = "Associated documents will not be deleted." activeModal.componentInstance.btnClass = "btn-danger" activeModal.componentInstance.btnCaption = "Delete" - activeModal.componentInstance.confirmPressed.subscribe(() => { + activeModal.componentInstance.confirmClicked.subscribe(() => { this.service.delete(object).subscribe(_ => { activeModal.close() this.reloadData() From 7f933d373f4639bf26379a8114013ab74bce6bf9 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Wed, 16 Dec 2020 19:50:38 +0100 Subject: [PATCH 0163/1708] fixes the decryption command not working. --- .../management/commands/decrypt_documents.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/documents/management/commands/decrypt_documents.py b/src/documents/management/commands/decrypt_documents.py index 918f1a175..8f5c2e123 100644 --- a/src/documents/management/commands/decrypt_documents.py +++ b/src/documents/management/commands/decrypt_documents.py @@ -2,7 +2,6 @@ import os from django.conf import settings from django.core.management.base import BaseCommand, CommandError -from termcolor import colored as coloured from documents.models import Document from paperless.db import GnuPG @@ -26,16 +25,14 @@ class Command(BaseCommand): def handle(self, *args, **options): try: - print(coloured( + print( "\n\nWARNING: This script is going to work directly on your " "document originals, so\nWARNING: you probably shouldn't run " "this unless you've got a recent backup\nWARNING: handy. It " "*should* work without a hitch, but be safe and backup your\n" "WARNING: stuff first.\n\nHit Ctrl+C to exit now, or Enter to " - "continue.\n\n", - "yellow", - attrs=("bold",) - )) + "continue.\n\n" + ) __ = input() except KeyboardInterrupt: return @@ -57,8 +54,8 @@ class Command(BaseCommand): for document in encrypted_files: - print(coloured("Decrypting {}".format( - document).encode('utf-8'), "green")) + print("Decrypting {}".format( + document).encode('utf-8')) old_paths = [document.source_path, document.thumbnail_path] From cf3fa50b55cb1ddd88921fc25b054b8cb5e8b38c Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Wed, 16 Dec 2020 21:08:03 +0100 Subject: [PATCH 0164/1708] these changes shouldn't have been commited at all. my bad. --- src/documents/admin.py | 2 +- .../migrations/1009_auto_20201216_2005.py | 29 +++++++++++++++++++ src/documents/models.py | 5 ++-- 3 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 src/documents/migrations/1009_auto_20201216_2005.py diff --git a/src/documents/admin.py b/src/documents/admin.py index 6ec3b736e..78437f91c 100755 --- a/src/documents/admin.py +++ b/src/documents/admin.py @@ -69,7 +69,7 @@ class DocumentAdmin(admin.ModelAdmin): filter_horizontal = ("tags",) - ordering = ["-created", "correspondent"] + ordering = ["-created"] date_hierarchy = "created" diff --git a/src/documents/migrations/1009_auto_20201216_2005.py b/src/documents/migrations/1009_auto_20201216_2005.py new file mode 100644 index 000000000..5e8302bb0 --- /dev/null +++ b/src/documents/migrations/1009_auto_20201216_2005.py @@ -0,0 +1,29 @@ +# Generated by Django 3.1.4 on 2020-12-16 20:05 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('documents', '1008_auto_20201216_1736'), + ] + + operations = [ + migrations.AlterModelOptions( + name='correspondent', + options={'ordering': ('name',)}, + ), + migrations.AlterModelOptions( + name='documenttype', + options={'ordering': ('name',)}, + ), + migrations.AlterModelOptions( + name='savedview', + options={'ordering': ('name',)}, + ), + migrations.AlterModelOptions( + name='tag', + options={'ordering': ('name',)}, + ), + ] diff --git a/src/documents/models.py b/src/documents/models.py index 02a293eeb..cede29b8e 100755 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -11,7 +11,6 @@ import dateutil.parser from django.conf import settings from django.contrib.auth.models import User from django.db import models -from django.db.models.functions import Lower from django.utils import timezone from documents.file_handling import archive_name_from_filename @@ -61,7 +60,7 @@ class MatchingModel(models.Model): class Meta: abstract = True - ordering = (Lower("name"),) + ordering = ("name",) def __str__(self): return self.name @@ -307,7 +306,7 @@ class SavedView(models.Model): class Meta: - ordering = (Lower("name"),) + ordering = ("name",) user = models.ForeignKey(User, on_delete=models.CASCADE) name = models.CharField(max_length=128) From 5c310c51d42c5c69a924e8f4887be25cfbe84e4d Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Wed, 16 Dec 2020 21:08:41 +0100 Subject: [PATCH 0165/1708] fix up the migration for encrypted documents. --- src/documents/migrations/1003_mime_types.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/documents/migrations/1003_mime_types.py b/src/documents/migrations/1003_mime_types.py index 1038d57b3..e5e613735 100644 --- a/src/documents/migrations/1003_mime_types.py +++ b/src/documents/migrations/1003_mime_types.py @@ -6,13 +6,17 @@ import magic from django.conf import settings from django.db import migrations, models +from paperless.db import GnuPG + +STORAGE_TYPE_UNENCRYPTED = "unencrypted" +STORAGE_TYPE_GPG = "gpg" def source_path(self): if self.filename: fname = str(self.filename) else: fname = "{:07}.{}".format(self.pk, self.file_type) - if self.storage_type == self.STORAGE_TYPE_GPG: + if self.storage_type == STORAGE_TYPE_GPG: fname += ".gpg" return os.path.join( @@ -26,9 +30,16 @@ def add_mime_types(apps, schema_editor): documents = Document.objects.all() for d in documents: - d.mime_type = magic.from_file(source_path(d), mime=True) + if d.storage_type == STORAGE_TYPE_GPG: + f = GnuPG.decrypted(open(source_path(d), "rb")) + else: + f = open(source_path(d), "rb") + + d.mime_type = magic.from_buffer(f.read(1024), mime=True) d.save() + f.close() + def add_file_extensions(apps, schema_editor): Document = apps.get_model("documents", "Document") From aa8789ae31c6f4fb0e8060be5618cf44381db6cd Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Wed, 16 Dec 2020 21:53:11 +0100 Subject: [PATCH 0166/1708] fix up the migration for encrypted documents and a couple other associated issues. --- src/documents/migrations/1003_mime_types.py | 10 ++++++---- src/documents/views.py | 14 ++++++++++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/documents/migrations/1003_mime_types.py b/src/documents/migrations/1003_mime_types.py index e5e613735..78ecced2b 100644 --- a/src/documents/migrations/1003_mime_types.py +++ b/src/documents/migrations/1003_mime_types.py @@ -30,12 +30,14 @@ def add_mime_types(apps, schema_editor): documents = Document.objects.all() for d in documents: + f = open(source_path(d), "rb") if d.storage_type == STORAGE_TYPE_GPG: - f = GnuPG.decrypted(open(source_path(d), "rb")) - else: - f = open(source_path(d), "rb") - d.mime_type = magic.from_buffer(f.read(1024), mime=True) + data = GnuPG.decrypted(f) + else: + data = f.read(1024) + + d.mime_type = magic.from_buffer(data, mime=True) d.save() f.close() diff --git a/src/documents/views.py b/src/documents/views.py index 36d3445c4..bf31c749b 100755 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -169,7 +169,12 @@ class DocumentViewSet(RetrieveModelMixin, parser_class = get_parser_class_for_mime_type(mime_type) if parser_class: parser = parser_class(logging_group=None) - return parser.extract_metadata(file, mime_type) + + try: + return parser.extract_metadata(file, mime_type) + except Exception as e: + # TODO: cover GPG errors, remove later. + return [] else: return [] @@ -215,7 +220,12 @@ class DocumentViewSet(RetrieveModelMixin, @cache_control(public=False, max_age=315360000) def thumb(self, request, pk=None): try: - return HttpResponse(Document.objects.get(id=pk).thumbnail_file, + doc = Document.objects.get(id=pk) + if doc.storage_type == Document.STORAGE_TYPE_GPG: + handle = GnuPG.decrypted(doc.thumbnail_file) + else: + handle = doc.thumbnail_file + return HttpResponse(handle, content_type='image/png') except (FileNotFoundError, Document.DoesNotExist): raise Http404() From e9affbc1cf2b7588fceeb75ef4d33df841f43380 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Wed, 16 Dec 2020 22:33:03 +0100 Subject: [PATCH 0167/1708] revert the changes that caused issues in the admin. --- src/documents/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/documents/models.py b/src/documents/models.py index cede29b8e..3a6d155ed 100755 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -78,6 +78,9 @@ class Correspondent(MatchingModel): # better safe than sorry. SAFE_REGEX = re.compile(r"^[\w\- ,.']+$") + class Meta: + ordering = ("name",) + class Tag(MatchingModel): From eaf11ea134624d387c1c47928344d4d0ce70a2f3 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Wed, 16 Dec 2020 22:39:13 +0100 Subject: [PATCH 0168/1708] changelog and versions --- docker/hub/docker-compose.postgres.yml | 2 +- docker/hub/docker-compose.sqlite.yml | 2 +- docs/changelog.rst | 9 +++++++++ src-ui/src/environments/environment.prod.ts | 2 +- src/paperless/version.py | 2 +- 5 files changed, 13 insertions(+), 4 deletions(-) diff --git a/docker/hub/docker-compose.postgres.yml b/docker/hub/docker-compose.postgres.yml index 6ab4b94a6..d33e4c38d 100644 --- a/docker/hub/docker-compose.postgres.yml +++ b/docker/hub/docker-compose.postgres.yml @@ -15,7 +15,7 @@ services: POSTGRES_PASSWORD: paperless webserver: - image: jonaswinkler/paperless-ng:0.9.7 + image: jonaswinkler/paperless-ng:0.9.8 restart: always depends_on: - db diff --git a/docker/hub/docker-compose.sqlite.yml b/docker/hub/docker-compose.sqlite.yml index 4e1da3e10..c130dfef6 100644 --- a/docker/hub/docker-compose.sqlite.yml +++ b/docker/hub/docker-compose.sqlite.yml @@ -5,7 +5,7 @@ services: restart: always webserver: - image: jonaswinkler/paperless-ng:0.9.7 + image: jonaswinkler/paperless-ng:0.9.8 restart: always depends_on: - broker diff --git a/docs/changelog.rst b/docs/changelog.rst index 330dd4cc9..a993eb530 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,15 @@ Changelog ********* +paperless-ng 0.9.8 +################## + +This release addresses two severe issues with the previous release. + +* The delete buttons for document types, correspondents and tags were not working. +* The document section in the admin was causing internal server errors (500). + + paperless-ng 0.9.7 ################## diff --git a/src-ui/src/environments/environment.prod.ts b/src-ui/src/environments/environment.prod.ts index 38699670e..f12c6a7cb 100644 --- a/src-ui/src/environments/environment.prod.ts +++ b/src-ui/src/environments/environment.prod.ts @@ -2,5 +2,5 @@ export const environment = { production: true, apiBaseUrl: "/api/", appTitle: "Paperless-ng", - version: "0.9.7" + version: "0.9.8" }; diff --git a/src/paperless/version.py b/src/paperless/version.py index 3c8636a10..10283c145 100644 --- a/src/paperless/version.py +++ b/src/paperless/version.py @@ -1 +1 @@ -__version__ = (0, 9, 7) +__version__ = (0, 9, 8) From 70347bb8f3f2e6e91df8a726d94e465621705ab1 Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Wed, 16 Dec 2020 23:26:29 +0100 Subject: [PATCH 0169/1708] added a note to the documentation regarding character limits of PostgreSQL --- docs/setup.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/setup.rst b/docs/setup.rst index e5e6526ea..e20b2e54a 100644 --- a/docs/setup.rst +++ b/docs/setup.rst @@ -460,6 +460,15 @@ management commands as below. load data from an old database schema in SQLite into a newer database schema in PostgreSQL, you will run into trouble. +.. warning:: + + On some database fields, PostgreSQL enforces predefined limits on maximum + length, whereas SQLite does not. The fields in question are the title of documents + (128 characters), names of document types, tags and correspondents (128 characters), + and filenames (1024 characters). If you have data in these fields that surpasses these + limits, migration to PostgreSQL is not possible and will fail with an error. + + 1. Stop paperless, if it is running. 2. Tell paperless to use PostgreSQL: From fbca412d309725f7dd1b1fc03c94b08b88da8218 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Wed, 16 Dec 2020 16:18:41 -0800 Subject: [PATCH 0170/1708] Add more card columns on very large screens --- .../document-list.component.html | 2 +- .../document-list.component.scss | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index 31b00f482..0b98a5633 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -116,6 +116,6 @@ </table> -<div class="m-n2 row row row-cols-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-5" *ngIf="displayMode == 'smallCards'"> +<div class="m-n2 row row-cols-paperless-cards" *ngIf="displayMode == 'smallCards'"> <app-document-card-small [document]="d" *ngFor="let d of list.documents" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)"></app-document-card-small> </div> diff --git a/src-ui/src/app/components/document-list/document-list.component.scss b/src-ui/src/app/components/document-list/document-list.component.scss index e69de29bb..08b88e0d0 100644 --- a/src-ui/src/app/components/document-list/document-list.component.scss +++ b/src-ui/src/app/components/document-list/document-list.component.scss @@ -0,0 +1,21 @@ +$paperless-card-breakpoints: ( + 0: 2, // xs + 768px: 3, //md + 992px: 4, //lg + 1200px: 5, //xl + 1400px: 6, // xxl + 1600px: 7, + 1800px: 8, + 2000px: 9 +); + +.row-cols-paperless-cards { + @each $width, $n_cols in $paperless-card-breakpoints { + @media(min-width: $width) { + > * { + flex: 0 0 auto; + width: 100% / $n_cols; + } + } + } +} From 164418880a93301b61e577363d4211b8142dcf0e Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Thu, 17 Dec 2020 21:36:21 +0100 Subject: [PATCH 0171/1708] more like this searching --- .../document-detail.component.html | 6 +++ .../document-detail.component.ts | 4 ++ .../document-card-large.component.html | 11 +++++- .../document-card-large.component.ts | 13 +++++++ .../result-highlight.component.scss | 2 +- .../components/search/search.component.html | 10 ++++- .../app/components/search/search.component.ts | 26 +++++++++++-- .../src/app/services/rest/search.service.ts | 10 ++++- src/documents/index.py | 37 ++++++++++++++----- src/documents/views.py | 21 +++++++---- 10 files changed, 113 insertions(+), 27 deletions(-) diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html index f4a64c2cc..f7e1ff855 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.html +++ b/src-ui/src/app/components/document-detail/document-detail.component.html @@ -24,6 +24,12 @@ </div> + <button type="button" class="btn btn-sm btn-outline-primary mr-2" (click)="moreLike()"> + <svg class="buttonicon" fill="currentColor"> + <use xlink:href="assets/bootstrap-icons.svg#three-dots" /> + </svg> + <span class="d-none d-lg-inline"> More like this</span> + </button> <button type="button" class="btn btn-sm btn-outline-primary" (click)="close()"> <svg class="buttonicon" fill="currentColor"> diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index b4005b920..90594bd0a 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -168,6 +168,10 @@ export class DocumentDetailComponent implements OnInit { } + moreLike() { + this.router.navigate(["search"], {queryParams: {more_like:this.document.id}}) + } + hasNext() { return this.documentListViewService.hasNext(this.documentId) } diff --git a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html index c2645db5e..32abaaef1 100644 --- a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html +++ b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html @@ -25,6 +25,12 @@ <div class="d-flex justify-content-between align-items-center"> <div class="btn-group"> + <a routerLink="/search" [queryParams]="{'more_like': document.id}" class="btn btn-sm btn-outline-secondary"> + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-three-dots" viewBox="0 0 16 16"> + <path fill-rule="evenodd" d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/> + </svg> + More like this + </a> <a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary"> <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> <path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/> @@ -45,10 +51,13 @@ </svg> Download </a> + <ngb-progressbar [type]="searchScoreClass" [value]="searchScore" style="width: 100px; height: 5px; margin: 10px;" [max]="1"></ngb-progressbar> + </div> + <small class="text-muted">Created: {{document.created | date}}</small> </div> - + </div> </div> </div> diff --git a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts index 2e056cc70..44f9cb906 100644 --- a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts +++ b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts @@ -24,6 +24,19 @@ export class DocumentCardLargeComponent implements OnInit { @Output() clickCorrespondent = new EventEmitter<number>() + @Input() + searchScore: number + + get searchScoreClass() { + if (this.searchScore > 0.7) { + return "success" + } else if (this.searchScore > 0.3) { + return "warning" + } else { + return "danger" + } + } + ngOnInit(): void { } diff --git a/src-ui/src/app/components/search/result-highlight/result-highlight.component.scss b/src-ui/src/app/components/search/result-highlight/result-highlight.component.scss index 645fb0426..e04dd13b2 100644 --- a/src-ui/src/app/components/search/result-highlight/result-highlight.component.scss +++ b/src-ui/src/app/components/search/result-highlight/result-highlight.component.scss @@ -1,4 +1,4 @@ .match { color: black; - background-color: orange; + background-color: rgb(255, 211, 66); } \ No newline at end of file diff --git a/src-ui/src/app/components/search/search.component.html b/src-ui/src/app/components/search/search.component.html index 55fcee900..609bea9e5 100644 --- a/src-ui/src/app/components/search/search.component.html +++ b/src-ui/src/app/components/search/search.component.html @@ -3,7 +3,12 @@ <div *ngIf="errorMessage" class="alert alert-danger">Invalid search query: {{errorMessage}}</div> -<p> +<p *ngIf="more_like"> + Showing documents similar to + <a routerLink="/documents/{{more_like}}">{{more_like_doc?.original_file_name}}</a> +</p> + +<p *ngIf="query"> Search string: <i>{{query}}</i> <ng-container *ngIf="correctedQuery"> - Did you mean "<a [routerLink]="" (click)="searchCorrectedQuery()">{{correctedQuery}}</a>"? @@ -15,7 +20,8 @@ <p>{{resultCount}} result(s)</p> <app-document-card-large *ngFor="let result of results" [document]="result.document" - [details]="result.highlights"> + [details]="result.highlights" + [searchScore]="result.score / maxScore"> </app-document-card-large> </div> diff --git a/src-ui/src/app/components/search/search.component.ts b/src-ui/src/app/components/search/search.component.ts index de8b4652f..b2b10d632 100644 --- a/src-ui/src/app/components/search/search.component.ts +++ b/src-ui/src/app/components/search/search.component.ts @@ -1,6 +1,9 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; +import { PaperlessDocument } from 'src/app/data/paperless-document'; +import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'; import { SearchHit } from 'src/app/data/search-result'; +import { DocumentService } from 'src/app/services/rest/document.service'; import { SearchService } from 'src/app/services/rest/search.service'; @Component({ @@ -14,6 +17,10 @@ export class SearchComponent implements OnInit { query: string = "" + more_like: number + + more_like_doc: PaperlessDocument + searching = false currentPage = 1 @@ -26,11 +33,23 @@ export class SearchComponent implements OnInit { errorMessage: string - constructor(private searchService: SearchService, private route: ActivatedRoute, private router: Router) { } + get maxScore() { + return this.results?.length > 0 ? this.results[0].score : 100 + } + + constructor(private searchService: SearchService, private route: ActivatedRoute, private router: Router, private documentService: DocumentService) { } ngOnInit(): void { this.route.queryParamMap.subscribe(paramMap => { this.query = paramMap.get('query') + this.more_like = paramMap.has('more_like') ? +paramMap.get('more_like') : null + if (this.more_like) { + this.documentService.get(this.more_like).subscribe(r => { + this.more_like_doc = r + }) + } else { + this.more_like_doc = null + } this.searching = true this.currentPage = 1 this.loadPage() @@ -39,13 +58,14 @@ export class SearchComponent implements OnInit { } searchCorrectedQuery() { - this.router.navigate(["search"], {queryParams: {query: this.correctedQuery}}) + this.router.navigate(["search"], {queryParams: {query: this.correctedQuery, more_like: this.more_like}}) } loadPage(append: boolean = false) { this.errorMessage = null this.correctedQuery = null - this.searchService.search(this.query, this.currentPage).subscribe(result => { + + this.searchService.search(this.query, this.currentPage, this.more_like).subscribe(result => { if (append) { this.results.push(...result.results) } else { diff --git a/src-ui/src/app/services/rest/search.service.ts b/src-ui/src/app/services/rest/search.service.ts index b19a55769..3799f3dc7 100644 --- a/src-ui/src/app/services/rest/search.service.ts +++ b/src-ui/src/app/services/rest/search.service.ts @@ -15,11 +15,17 @@ export class SearchService { constructor(private http: HttpClient, private documentService: DocumentService) { } - search(query: string, page?: number): Observable<SearchResult> { - let httpParams = new HttpParams().set('query', query) + search(query: string, page?: number, more_like?: number): Observable<SearchResult> { + let httpParams = new HttpParams() + if (query) { + httpParams = httpParams.set('query', query) + } if (page) { httpParams = httpParams.set('page', page.toString()) } + if (more_like) { + httpParams = httpParams.set('more_like', more_like.toString()) + } return this.http.get<SearchResult>(`${environment.apiBaseUrl}search/`, {params: httpParams}).pipe( map(result => { result.results.forEach(hit => this.documentService.addObservablesToDocument(hit.document)) diff --git a/src/documents/index.py b/src/documents/index.py index 53bf34542..7d022182f 100644 --- a/src/documents/index.py +++ b/src/documents/index.py @@ -3,7 +3,7 @@ import os from contextlib import contextmanager from django.conf import settings -from whoosh import highlight +from whoosh import highlight, classify, query from whoosh.fields import Schema, TEXT, NUMERIC, KEYWORD, DATETIME from whoosh.highlight import Formatter, get_text from whoosh.index import create_in, exists_in, open_dir @@ -120,22 +120,39 @@ def remove_document_from_index(document): @contextmanager -def query_page(ix, querystring, page): +def query_page(ix, page, querystring, more_like_doc_id, more_like_doc_content): searcher = ix.searcher() try: - qp = MultifieldParser( - ["content", "title", "correspondent", "tag", "type"], - ix.schema) - qp.add_plugin(DateParserPlugin()) + if querystring: + qp = MultifieldParser( + ["content", "title", "correspondent", "tag", "type"], + ix.schema) + qp.add_plugin(DateParserPlugin()) + str_q = qp.parse(querystring) + corrected = searcher.correct_query(str_q, querystring) + else: + str_q = None + corrected = None + + if more_like_doc_id: + docnum = searcher.document_number(id=more_like_doc_id) + kts = searcher.key_terms_from_text('content', more_like_doc_content, numterms=20, + model=classify.Bo1Model, normalize=False) + more_like_q = query.Or([query.Term('content', word, boost=weight) + for word, weight in kts]) + result_page = searcher.search_page(more_like_q, page, filter=str_q, mask={docnum}) + elif str_q: + result_page = searcher.search_page(str_q, page) + else: + raise ValueError( + "Either querystring or more_like_doc_id is required." + ) - q = qp.parse(querystring) - result_page = searcher.search_page(q, page) result_page.results.fragmenter = highlight.ContextFragmenter( surround=50) result_page.results.formatter = JsonFormatter() - corrected = searcher.correct_query(q, querystring) - if corrected.query != q: + if corrected and corrected.query != str_q: corrected_query = corrected.string else: corrected_query = None diff --git a/src/documents/views.py b/src/documents/views.py index bf31c749b..bd9a748e8 100755 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -335,14 +335,19 @@ class SearchView(APIView): } def get(self, request, format=None): - if 'query' not in request.query_params: - return Response({ - 'count': 0, - 'page': 0, - 'page_count': 0, - 'results': []}) - query = request.query_params['query'] + if 'query' in request.query_params: + query = request.query_params['query'] + else: + query = None + + if 'more_like' in request.query_params: + more_like_id = request.query_params['more_like'] + more_like_content = Document.objects.get(id=more_like_id).content + else: + more_like_id = None + more_like_content = None + try: page = int(request.query_params.get('page', 1)) except (ValueError, TypeError): @@ -352,7 +357,7 @@ class SearchView(APIView): page = 1 try: - with index.query_page(self.ix, query, page) as (result_page, + with index.query_page(self.ix, page, query, more_like_id, more_like_content) as (result_page, corrected_query): return Response( {'count': len(result_page), From 48796e6961b0f61366e3ff77f2b78a371153768e Mon Sep 17 00:00:00 2001 From: jonaswinkler <jonas.winkler@jpwinkler.de> Date: Thu, 17 Dec 2020 21:46:56 +0100 Subject: [PATCH 0172/1708] fixes #149 --- src-ui/src/app/interceptors/csrf.interceptor.ts | 7 +++++-- src/documents/templates/index.html | 1 + src/documents/views.py | 5 +++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src-ui/src/app/interceptors/csrf.interceptor.ts b/src-ui/src/app/interceptors/csrf.interceptor.ts index 32f3e99dc..2ef03dc56 100644 --- a/src-ui/src/app/interceptors/csrf.interceptor.ts +++ b/src-ui/src/app/interceptors/csrf.interceptor.ts @@ -7,16 +7,19 @@ import { } from '@angular/common/http'; import { Observable } from 'rxjs'; import { CookieService } from 'ngx-cookie-service'; +import { Meta } from '@angular/platform-browser'; @Injectable() export class CsrfInterceptor implements HttpInterceptor { - constructor(private cookieService: CookieService) { + constructor(private cookieService: CookieService, private meta: Meta) { } intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> { - let csrfToken = this.cookieService.get('csrftoken') + + let prefix = this.meta.getTag('name=cookie_prefix').content + let csrfToken = this.cookieService.get(`${prefix?prefix:''}csrftoken`) if (csrfToken) { request = request.clone({ setHeaders: { diff --git a/src/documents/templates/index.html b/src/documents/templates/index.html index 728f3a0e7..06dbb678e 100644 --- a/src/documents/templates/index.html +++ b/src/documents/templates/index.html @@ -8,6 +8,7 @@ <title>PaperlessUi + diff --git a/src/documents/views.py b/src/documents/views.py index bf31c749b..f90e9f7bc 100755 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -54,6 +54,11 @@ from .serialisers import ( class IndexView(TemplateView): template_name = "index.html" + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['cookie_prefix'] = settings.COOKIE_PREFIX + return context + class CorrespondentViewSet(ModelViewSet): model = Correspondent From 35dcc54dc875f5015ac003f7701f4f4c04aa8350 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Thu, 17 Dec 2020 21:54:05 +0100 Subject: [PATCH 0173/1708] fixes cookie_prefix for development setups --- src-ui/src/app/interceptors/csrf.interceptor.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src-ui/src/app/interceptors/csrf.interceptor.ts b/src-ui/src/app/interceptors/csrf.interceptor.ts index 2ef03dc56..2c654aa36 100644 --- a/src-ui/src/app/interceptors/csrf.interceptor.ts +++ b/src-ui/src/app/interceptors/csrf.interceptor.ts @@ -17,8 +17,10 @@ export class CsrfInterceptor implements HttpInterceptor { } intercept(request: HttpRequest, next: HttpHandler): Observable> { - - let prefix = this.meta.getTag('name=cookie_prefix').content + let prefix = "" + if (this.meta.getTag('name=cookie_prefix')) { + prefix = this.meta.getTag('name=cookie_prefix').content + } let csrfToken = this.cookieService.get(`${prefix?prefix:''}csrftoken`) if (csrfToken) { request = request.clone({ From 2c3eaadbce9fac1a465c0e4c866f9df3ae4216b2 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Thu, 17 Dec 2020 23:24:28 +0100 Subject: [PATCH 0174/1708] test cases --- src/documents/tests/test_api.py | 19 +++++++++++++++++++ src/documents/views.py | 8 ++++++++ 2 files changed, 27 insertions(+) diff --git a/src/documents/tests/test_api.py b/src/documents/tests/test_api.py index 49dddee87..ba1ab45ca 100644 --- a/src/documents/tests/test_api.py +++ b/src/documents/tests/test_api.py @@ -351,6 +351,25 @@ class TestDocumentApi(DirectoriesMixin, APITestCase): self.assertEqual(correction, None) + def test_search_more_like(self): + d1=Document.objects.create(title="invoice", content="the thing i bought at a shop and paid with bank account", checksum="A", pk=1) + d2=Document.objects.create(title="bank statement 1", content="things i paid for in august", pk=2, checksum="B") + d3=Document.objects.create(title="bank statement 3", content="things i paid for in september", pk=3, checksum="C") + with AsyncWriter(index.open_index()) as writer: + index.update_document(writer, d1) + index.update_document(writer, d2) + index.update_document(writer, d3) + + response = self.client.get(f"/api/search/?more_like={d2.id}") + + self.assertEqual(response.status_code, 200) + + results = response.data['results'] + + self.assertEqual(len(results), 2) + self.assertEqual(results[0]['id'], d3.id) + self.assertEqual(results[1]['id'], d1.id) + def test_statistics(self): doc1 = Document.objects.create(title="none1", checksum="A") diff --git a/src/documents/views.py b/src/documents/views.py index bd9a748e8..59fbfb213 100755 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -348,6 +348,14 @@ class SearchView(APIView): more_like_id = None more_like_content = None + if not query and not more_like_id: + return Response({ + 'count': 0, + 'page': 0, + 'page_count': 0, + 'corrected_query': None, + 'results': []}) + try: page = int(request.query_params.get('page', 1)) except (ValueError, TypeError): From 659cd3e9d5362d20b44c0f9112aec2e0a5fd7dfe Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Thu, 17 Dec 2020 23:41:46 +0100 Subject: [PATCH 0175/1708] hide search controls on document list --- .../document-card-large/document-card-large.component.html | 4 ++-- .../document-card-large/document-card-large.component.scss | 6 ++++++ .../document-card-large/document-card-large.component.ts | 3 +++ src-ui/src/app/components/search/search.component.html | 3 ++- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html index 32abaaef1..58c0f6241 100644 --- a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html +++ b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html @@ -25,7 +25,7 @@
diff --git a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.scss b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.scss index 11fb10562..438d2c768 100644 --- a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.scss +++ b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.scss @@ -9,4 +9,10 @@ height: 100%; position: absolute; +} + +.search-score-bar { + width: 100px; + height: 5px; + margin: 10px; } \ No newline at end of file diff --git a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts index 44f9cb906..bcc1b1f3c 100644 --- a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts +++ b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts @@ -12,6 +12,9 @@ export class DocumentCardLargeComponent implements OnInit { constructor(private documentService: DocumentService, private sanitizer: DomSanitizer) { } + @Input() + moreLikeThis: boolean = false + @Input() document: PaperlessDocument diff --git a/src-ui/src/app/components/search/search.component.html b/src-ui/src/app/components/search/search.component.html index 609bea9e5..de6f0133f 100644 --- a/src-ui/src/app/components/search/search.component.html +++ b/src-ui/src/app/components/search/search.component.html @@ -21,7 +21,8 @@ + [searchScore]="result.score / maxScore" + [moreLikeThis]="true">
From 93be4e98d5ad79e6bf602e866937c8d1192798f7 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Thu, 17 Dec 2020 23:41:55 +0100 Subject: [PATCH 0176/1708] scroll to top when searching again --- src-ui/src/app/components/search/search.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src-ui/src/app/components/search/search.component.ts b/src-ui/src/app/components/search/search.component.ts index b2b10d632..4570ac3fa 100644 --- a/src-ui/src/app/components/search/search.component.ts +++ b/src-ui/src/app/components/search/search.component.ts @@ -41,6 +41,7 @@ export class SearchComponent implements OnInit { ngOnInit(): void { this.route.queryParamMap.subscribe(paramMap => { + window.scrollTo(0, 0) this.query = paramMap.get('query') this.more_like = paramMap.has('more_like') ? +paramMap.get('more_like') : null if (this.more_like) { From ca2cb694d0dbd70c550c8dc9e69a578e9fe6fd4e Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Fri, 18 Dec 2020 00:10:16 +0100 Subject: [PATCH 0177/1708] code style --- src/documents/index.py | 13 ++++++++----- src/documents/views.py | 3 +-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/documents/index.py b/src/documents/index.py index 7d022182f..fdf7d7041 100644 --- a/src/documents/index.py +++ b/src/documents/index.py @@ -136,11 +136,14 @@ def query_page(ix, page, querystring, more_like_doc_id, more_like_doc_content): if more_like_doc_id: docnum = searcher.document_number(id=more_like_doc_id) - kts = searcher.key_terms_from_text('content', more_like_doc_content, numterms=20, - model=classify.Bo1Model, normalize=False) - more_like_q = query.Or([query.Term('content', word, boost=weight) - for word, weight in kts]) - result_page = searcher.search_page(more_like_q, page, filter=str_q, mask={docnum}) + kts = searcher.key_terms_from_text( + 'content', more_like_doc_content, numterms=20, + model=classify.Bo1Model, normalize=False) + more_like_q = query.Or( + [query.Term('content', word, boost=weight) + for word, weight in kts]) + result_page = searcher.search_page( + more_like_q, page, filter=str_q, mask={docnum}) elif str_q: result_page = searcher.search_page(str_q, page) else: diff --git a/src/documents/views.py b/src/documents/views.py index 1f5489999..54d0de3f6 100755 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -370,8 +370,7 @@ class SearchView(APIView): page = 1 try: - with index.query_page(self.ix, page, query, more_like_id, more_like_content) as (result_page, - corrected_query): + with index.query_page(self.ix, page, query, more_like_id, more_like_content) as (result_page, corrected_query): # NOQA: E501 return Response( {'count': len(result_page), 'page': result_page.pagenum, From 1c4e3f682e867d52e795b8b97dc7649c746b1a99 Mon Sep 17 00:00:00 2001 From: Jonas Winkler Date: Fri, 18 Dec 2020 01:31:46 +0100 Subject: [PATCH 0178/1708] Update README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e8ae8feb2..fca1cd2cf 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,8 @@ For a complete list of changes from paperless, check out the [changelog](https:/ # Roadmap for 1.0 +- **Bulk editing**. Add/remove metadata from multiple documents at once. + - Make the front end nice (except mobile). - Test coverage at 90%. - Fix whatever bugs I and you find. @@ -59,7 +61,6 @@ For a complete list of changes from paperless, check out the [changelog](https:/ These are things that I want to add to paperless eventually. They are sorted by priority. -- **Bulk editing**. Add/remove metadata from multiple documents at once. - **More search.** The search backend is incredibly versatile and customizable. Searching is the most important feature of this project and thus, I want to implement things like: - Group and limit search results by correspondent, show “more from this” links in the results. - Ability to search for “Similar documents” in the search results @@ -68,6 +69,9 @@ These are things that I want to add to paperless eventually. They are sorted by - 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. - 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. +- **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. ## On the chopping block. From cfc1ca45fc599a273d360078b160f688e5c19a5f Mon Sep 17 00:00:00 2001 From: Jonas Winkler Date: Fri, 18 Dec 2020 01:35:08 +0100 Subject: [PATCH 0179/1708] Update README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fca1cd2cf..32ff2ab4a 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,10 @@ Here's what you get: * Includes a dashboard that shows basic statistics and has document upload. * Filtering by tags, correspondents, types, and more. * Customizable views can be saved and displayed on the dashboard. - * Full text search with auto completion, scored results and query highlighting allows you to quickly find what you need. +* Full text search helps you find what you need. + * Auto completion suggests relevant words from your documents. + * Results are sorted by relevance to your search query. + * Highlighting shows you which parts of the document matched the query. * Email processing: Paperless adds documents from your email accounts. * Configure multiple accounts and filters for each account. * When adding documents from mails, paperless can move these mails to a new folder, mark them as read, flag them or delete them. From 9b244d02655c7837323633b71826a89473b6e19a Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Thu, 17 Dec 2020 23:09:27 -0800 Subject: [PATCH 0180/1708] Use ng-select for document detail screen --- src-ui/package-lock.json | 8 +++++++ src-ui/package.json | 1 + src-ui/src/app/app.module.ts | 4 +++- .../common/input/select/select.component.html | 18 +++++++++------ .../common/input/select/select.component.scss | 1 + src-ui/src/styles.scss | 23 ++++++++++++++++++- 6 files changed, 46 insertions(+), 9 deletions(-) diff --git a/src-ui/package-lock.json b/src-ui/package-lock.json index 5eca0b3c0..10215a32d 100644 --- a/src-ui/package-lock.json +++ b/src-ui/package-lock.json @@ -2056,6 +2056,14 @@ "tslib": "^2.0.0" } }, + "@ng-select/ng-select": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-5.0.9.tgz", + "integrity": "sha512-YZeSAiS8/Nx/eHZJPmOOYL8YmcvSq+dr1P8WIrsKmRA7mueorBpPc5xlUj+nLQbpLtsiQvdWDQspf/ykOvD/lA==", + "requires": { + "tslib": "^2.0.0" + } + }, "@ngtools/webpack": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-10.2.0.tgz", diff --git a/src-ui/package.json b/src-ui/package.json index 6293f2672..14d828483 100644 --- a/src-ui/package.json +++ b/src-ui/package.json @@ -21,6 +21,7 @@ "@angular/platform-browser-dynamic": "~10.1.5", "@angular/router": "~10.1.5", "@ng-bootstrap/ng-bootstrap": "^8.0.0", + "@ng-select/ng-select": "^5.0.9", "bootstrap": "^4.5.0", "ng-bootstrap": "^1.6.3", "ng2-pdf-viewer": "^6.3.2", diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index 3c00cd0b7..d9c3800d6 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -54,6 +54,7 @@ import { FileSizePipe } from './pipes/file-size.pipe'; import { FilterPipe } from './pipes/filter.pipe'; import { DocumentTitlePipe } from './pipes/document-title.pipe'; import { MetadataCollapseComponent } from './components/document-detail/metadata-collapse/metadata-collapse.component'; +import { NgSelectModule } from '@ng-select/ng-select'; @NgModule({ declarations: [ @@ -110,7 +111,8 @@ import { MetadataCollapseComponent } from './components/document-detail/metadata ReactiveFormsModule, NgxFileDropModule, InfiniteScrollModule, - PdfViewerModule + PdfViewerModule, + NgSelectModule ], providers: [ DatePipe, diff --git a/src-ui/src/app/components/common/input/select/select.component.html b/src-ui/src/app/components/common/input/select/select.component.html index 717aa7964..655adbe74 100644 --- a/src-ui/src/app/components/common/input/select/select.component.html +++ b/src-ui/src/app/components/common/input/select/select.component.html @@ -1,11 +1,15 @@ -
+
- + + {{i.name}} + +
{{hint}} -
\ No newline at end of file +
diff --git a/src-ui/src/app/components/common/input/select/select.component.scss b/src-ui/src/app/components/common/input/select/select.component.scss index e69de29bb..8faec3bc0 100644 --- a/src-ui/src/app/components/common/input/select/select.component.scss +++ b/src-ui/src/app/components/common/input/select/select.component.scss @@ -0,0 +1 @@ +// styles for ng-select child are in styles.scss diff --git a/src-ui/src/styles.scss b/src-ui/src/styles.scss index b0b66b7f9..2eeb40d41 100644 --- a/src-ui/src/styles.scss +++ b/src-ui/src/styles.scss @@ -2,6 +2,7 @@ @import "node_modules/bootstrap/scss/bootstrap"; +@import "~@ng-select/ng-select/themes/default.theme.css"; .toolbaricon { width: 1.2em; @@ -65,4 +66,24 @@ body { display: block; background-size: 1rem; float: right; -} \ No newline at end of file +} + +.paperless-input-select { + .ng-select { + position: relative; + flex: 1 1 auto; + margin-bottom: 0; + height: calc(1.5em + 0.75rem + 5px); + line-height: 1.5; + + .ng-select-container { + height: 100%; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + + .ng-value-container .ng-input { + top: 8px; + } + } + } +} From e10a2391c44dcb48db992b0d477a8f150a0bb481 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Fri, 18 Dec 2020 00:53:01 -0800 Subject: [PATCH 0181/1708] Use ng-select for document detail screen tags --- .../common/input/tags/tags.component.html | 36 ++++++++++--------- .../common/input/tags/tags.component.scss | 12 ++----- .../common/input/tags/tags.component.ts | 23 ++++++------ .../document-detail.component.html | 4 +-- src-ui/src/styles.scss | 15 ++++++-- 5 files changed, 48 insertions(+), 42 deletions(-) diff --git a/src-ui/src/app/components/common/input/tags/tags.component.html b/src-ui/src/app/components/common/input/tags/tags.component.html index 8029dd860..89e391813 100644 --- a/src-ui/src/app/components/common/input/tags/tags.component.html +++ b/src-ui/src/app/components/common/input/tags/tags.component.html @@ -1,30 +1,34 @@ -
- +
+
-
- -
+ -
- -
- -
-
+ + + + +
+ + + +
+ +
+
-
-
{{hint}} -
\ No newline at end of file +
diff --git a/src-ui/src/app/components/common/input/tags/tags.component.scss b/src-ui/src/app/components/common/input/tags/tags.component.scss index f2635b7f2..41fc6acc4 100644 --- a/src-ui/src/app/components/common/input/tags/tags.component.scss +++ b/src-ui/src/app/components/common/input/tags/tags.component.scss @@ -1,10 +1,4 @@ -.tags-form-control { - height: auto; +.selected-icon { + min-width: 1em; + min-height: 1em; } - - -.scrollable-menu { - height: auto; - max-height: 300px; - overflow-x: hidden; -} \ No newline at end of file diff --git a/src-ui/src/app/components/common/input/tags/tags.component.ts b/src-ui/src/app/components/common/input/tags/tags.component.ts index cca99cc55..5501ac5a6 100644 --- a/src-ui/src/app/components/common/input/tags/tags.component.ts +++ b/src-ui/src/app/components/common/input/tags/tags.component.ts @@ -21,7 +21,7 @@ export class TagsComponent implements OnInit, ControlValueAccessor { onChange = (newValue: number[]) => {}; - + onTouched = () => {}; writeValue(newValue: number[]): void { @@ -66,29 +66,28 @@ export class TagsComponent implements OnInit, ControlValueAccessor { removeTag(id) { let index = this.displayValue.indexOf(id) if (index > -1) { - this.displayValue.splice(index, 1) + let oldValue = this.displayValue + oldValue.splice(index, 1) + this.displayValue = [...oldValue] this.onChange(this.displayValue) } } - addTag(id) { - let index = this.displayValue.indexOf(id) - if (index == -1) { - this.displayValue.push(id) - this.onChange(this.displayValue) - } - } - - createTag() { var modal = this.modalService.open(TagEditDialogComponent, {backdrop: 'static'}) modal.componentInstance.dialogMode = 'create' modal.componentInstance.success.subscribe(newTag => { this.tagService.listAll().subscribe(tags => { this.tags = tags.results - this.addTag(newTag.id) + this.displayValue = [...this.displayValue, newTag.id] + this.onChange(this.displayValue) }) }) } + ngSelectChange() { + this.value = this.displayValue + this.onChange(this.displayValue) + } + } diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html index f4a64c2cc..a3bc7e1e6 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.html +++ b/src-ui/src/app/components/document-detail/document-detail.component.html @@ -52,9 +52,9 @@

+ (createNew)="createCorrespondent()"> + (createNew)="createDocumentType()"> diff --git a/src-ui/src/styles.scss b/src-ui/src/styles.scss index 2eeb40d41..0dc662e31 100644 --- a/src-ui/src/styles.scss +++ b/src-ui/src/styles.scss @@ -1,7 +1,5 @@ @import "theme"; - @import "node_modules/bootstrap/scss/bootstrap"; - @import "~@ng-select/ng-select/themes/default.theme.css"; .toolbaricon { @@ -21,7 +19,7 @@ } body { - font-size: .875rem; + font-size: 0.875rem; } .form-control-dark { @@ -85,5 +83,16 @@ body { top: 8px; } } + + .ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-selected, + .ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-selected.ng-option-marked { + background: none; + } + } +} + +.paperless-input-tags { + .ng-select.ng-select-multiple .ng-select-container .ng-value-container .ng-value { + background-color: transparent; } } From 55c4c690ef8eec3ba494cd6c3a74dc9b5cd810ec Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Fri, 18 Dec 2020 01:13:30 -0800 Subject: [PATCH 0182/1708] Fix wrapping with multiple tags, embiggen tags, pretty icons --- .../common/input/tags/tags.component.html | 21 ++++++++++++------- .../common/input/tags/tags.component.scss | 8 +++++++ src-ui/src/styles.scss | 2 +- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src-ui/src/app/components/common/input/tags/tags.component.html b/src-ui/src/app/components/common/input/tags/tags.component.html index 89e391813..8a5dbc4f2 100644 --- a/src-ui/src/app/components/common/input/tags/tags.component.html +++ b/src-ui/src/app/components/common/input/tags/tags.component.html @@ -1,7 +1,7 @@
-
+
- + + + + + + -
- - - +
+
+ + + +
+
- diff --git a/src-ui/src/app/components/common/input/tags/tags.component.scss b/src-ui/src/app/components/common/input/tags/tags.component.scss index 41fc6acc4..2eaaa4f6d 100644 --- a/src-ui/src/app/components/common/input/tags/tags.component.scss +++ b/src-ui/src/app/components/common/input/tags/tags.component.scss @@ -2,3 +2,11 @@ min-width: 1em; min-height: 1em; } + +.tag-wrap { + font-size: 1rem; +} + +.tag-wrap-delete { + cursor: pointer; +} diff --git a/src-ui/src/styles.scss b/src-ui/src/styles.scss index 0dc662e31..ffb296271 100644 --- a/src-ui/src/styles.scss +++ b/src-ui/src/styles.scss @@ -71,7 +71,7 @@ body { position: relative; flex: 1 1 auto; margin-bottom: 0; - height: calc(1.5em + 0.75rem + 5px); + min-height: calc(1.5em + 0.75rem + 5px); line-height: 1.5; .ng-select-container { From c05de3d57f6148d4abee8f8775d6668f6984ed2d Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Fri, 18 Dec 2020 01:18:11 -0800 Subject: [PATCH 0183/1708] Tiny padding fixes --- src-ui/src/styles.scss | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src-ui/src/styles.scss b/src-ui/src/styles.scss index ffb296271..6e09db630 100644 --- a/src-ui/src/styles.scss +++ b/src-ui/src/styles.scss @@ -80,7 +80,7 @@ body { border-bottom-right-radius: 0; .ng-value-container .ng-input { - top: 8px; + top: 10px; } } @@ -95,4 +95,8 @@ body { .ng-select.ng-select-multiple .ng-select-container .ng-value-container .ng-value { background-color: transparent; } + + .ng-select.ng-select-multiple .ng-select-container .ng-value-container { + padding-top: 1px; + } } From 273c474e3fe00f68898c1dc74256a2f4de7174e9 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Fri, 18 Dec 2020 14:09:12 +0100 Subject: [PATCH 0184/1708] layout changes --- .../document-card-large/document-card-large.component.html | 7 +++++-- .../document-card-large/document-card-large.component.scss | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html index 58c0f6241..5bf0c9af2 100644 --- a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html +++ b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html @@ -23,7 +23,7 @@

-
+
+ + Score: + + Created: {{document.created | date}}
diff --git a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.scss b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.scss index 438d2c768..a20a56672 100644 --- a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.scss +++ b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.scss @@ -14,5 +14,5 @@ .search-score-bar { width: 100px; height: 5px; - margin: 10px; + margin-top: 2px; } \ No newline at end of file From 789abb3bbb3c14b85952e98485cf98c289deb9c0 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Fri, 18 Dec 2020 16:42:33 +0100 Subject: [PATCH 0185/1708] changed up the highlight fragment formatter --- docs/api.rst | 15 ++++------ .../result-highlight.component.html | 2 +- src/documents/index.py | 29 +++++++++++-------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index d352758fa..cff72a970 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -221,21 +221,16 @@ Each fragment contains a list of strings, and some of them are marked as a highl [ [ - {"text": "This is a sample text with a "}, - {"text": "highlighted", "term": 0}, - {"text": " word."} + {"text": "This is a sample text with a ", "highlight": false}, + {"text": "highlighted", "highlight": true}, + {"text": " word.", "highlight": false} ], [ - {"text": "Another", "term": 1}, - {"text": " fragment with a highlight."} + {"text": "Another", "highlight": true}, + {"text": " fragment with a highlight.", "highlight": false} ] ] - - -When ``term`` is present within a string, the word within ``text`` should be highlighted. -The term index groups multiple matches together and words with the same index -should get identical highlighting. A client may use this example to produce the following output: ... This is a sample text with a **highlighted** word. ... **Another** fragment with a highlight. ... diff --git a/src-ui/src/app/components/search/result-highlight/result-highlight.component.html b/src-ui/src/app/components/search/result-highlight/result-highlight.component.html index 1842f5cea..5dc5baa94 100644 --- a/src-ui/src/app/components/search/result-highlight/result-highlight.component.html +++ b/src-ui/src/app/components/search/result-highlight/result-highlight.component.html @@ -1,3 +1,3 @@ ... - {{token.text}} ... + {{token.text}} ... \ No newline at end of file diff --git a/src/documents/index.py b/src/documents/index.py index fdf7d7041..308ee932e 100644 --- a/src/documents/index.py +++ b/src/documents/index.py @@ -20,32 +20,37 @@ class JsonFormatter(Formatter): self.seen = {} def format_token(self, text, token, replace=False): - seen = self.seen ttext = self._text(get_text(text, token, replace)) - if ttext in seen: - termnum = seen[ttext] - else: - termnum = len(seen) - seen[ttext] = termnum - - return {'text': ttext, 'term': termnum} + return {'text': ttext, 'highlight': 'true'} def format_fragment(self, fragment, replace=False): output = [] index = fragment.startchar text = fragment.text - + amend_token = None for t in fragment.matches: if t.startchar is None: continue if t.startchar < index: continue if t.startchar > index: - output.append({'text': text[index:t.startchar]}) - output.append(self.format_token(text, t, replace)) + text_inbetween = text[index:t.startchar] + if amend_token and t.startchar - index < 10: + amend_token['text'] += text_inbetween + else: + output.append({'text': text_inbetween, + 'highlight': False}) + amend_token = None + token = self.format_token(text, t, replace) + if amend_token: + amend_token['text'] += token['text'] + else: + output.append(token) + amend_token = token index = t.endchar if index < fragment.endchar: - output.append({'text': text[index:fragment.endchar]}) + output.append({'text': text[index:fragment.endchar], + 'highlight': False}) return output def format(self, fragments, replace=False): From dfb88ebf8328ef9ed6b71c9da4b9ddac49768ea5 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Fri, 18 Dec 2020 20:17:17 +0100 Subject: [PATCH 0186/1708] removed the date hack. fixes #144 also refer to #148 --- .../filter-dropdown-date.component.html | 51 +++---- .../filter-dropdown-date.component.ts | 139 ++++++++---------- .../filter-editor/filter-editor.component.ts | 31 ++-- 3 files changed, 106 insertions(+), 115 deletions(-) diff --git a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html index 6f6a42fe2..aca6e836c 100644 --- a/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html +++ b/src-ui/src/app/components/filter-editor/filter-dropdown-date/filter-dropdown-date.component.html @@ -4,38 +4,39 @@ diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index b4005b920..a2f80f786 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -15,6 +15,7 @@ import { DocumentService } from 'src/app/services/rest/document.service'; import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'; import { CorrespondentEditDialogComponent } from '../manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component'; import { DocumentTypeEditDialogComponent } from '../manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component'; +import { PDFDocumentProxy } from 'ng2-pdf-viewer'; @Component({ selector: 'app-document-detail', @@ -47,8 +48,11 @@ export class DocumentDetailComponent implements OnInit { tags: new FormControl([]) }) + currentPreviewPage: number = 1 + previewNumPages: number + constructor( - private documentsService: DocumentService, + private documentsService: DocumentService, private route: ActivatedRoute, private correspondentService: CorrespondentService, private documentTypeService: DocumentTypeService, @@ -113,6 +117,8 @@ export class DocumentDetailComponent implements OnInit { modal.componentInstance.success.subscribe(newCorrespondent => { this.correspondentService.listAll().subscribe(correspondents => { this.correspondents = correspondents.results + console.log(this.documentForm.get('correspondent'), this.documentForm.get('correspondent').setValue); + this.documentForm.get('correspondent').setValue(newCorrespondent.id) }) }) @@ -126,7 +132,7 @@ export class DocumentDetailComponent implements OnInit { }, error => {this.router.navigate(['404'])}) } - save() { + save() { this.documentsService.update(this.document).subscribe(result => { this.close() }) @@ -161,7 +167,7 @@ export class DocumentDetailComponent implements OnInit { modal.componentInstance.btnCaption = "Delete document" modal.componentInstance.confirmClicked.subscribe(() => { this.documentsService.delete(this.document).subscribe(() => { - modal.close() + modal.close() this.close() }) }) @@ -171,4 +177,9 @@ export class DocumentDetailComponent implements OnInit { hasNext() { return this.documentListViewService.hasNext(this.documentId) } + + pdfPreviewLoaded(pdf: PDFDocumentProxy) { + this.previewNumPages = pdf.numPages + } + } From f214fe1b3eb5b8b319d7fabeeb847d7a96cddb50 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Fri, 18 Dec 2020 14:44:17 -0800 Subject: [PATCH 0188/1708] Log line --- .../app/components/document-detail/document-detail.component.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index a2f80f786..aa3d4e5b8 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -117,8 +117,6 @@ export class DocumentDetailComponent implements OnInit { modal.componentInstance.success.subscribe(newCorrespondent => { this.correspondentService.listAll().subscribe(correspondents => { this.correspondents = correspondents.results - console.log(this.documentForm.get('correspondent'), this.documentForm.get('correspondent').setValue); - this.documentForm.get('correspondent').setValue(newCorrespondent.id) }) }) From 2d841e71673559e8853e4f853ffec96e004f8e0a Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Fri, 18 Dec 2020 14:47:06 -0800 Subject: [PATCH 0189/1708] Refactor --- .../components/document-detail/document-detail.component.html | 4 ++-- .../components/document-detail/document-detail.component.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html index 0fec2aa44..e5dde2ad0 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.html +++ b/src-ui/src/app/components/document-detail/document-detail.component.html @@ -3,7 +3,7 @@
Page
- +
of {{previewNumPages}}
@@ -138,7 +138,7 @@
- +
diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index aa3d4e5b8..2b839e969 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -48,7 +48,7 @@ export class DocumentDetailComponent implements OnInit { tags: new FormControl([]) }) - currentPreviewPage: number = 1 + previewCurrentPage: number = 1 previewNumPages: number constructor( From fbb2da42dc92cabe2379b890e8c3ea0a1fb7d591 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Fri, 18 Dec 2020 14:55:21 -0800 Subject: [PATCH 0190/1708] Merge branch 'dev' into feature-bulk-editor --- README.md | 25 ++- docker/hub/docker-compose.postgres.yml | 2 +- docker/hub/docker-compose.sqlite.yml | 2 +- docs/advanced_usage.rst | 36 ++-- docs/changelog.rst | 54 +++++ docs/usage_overview.rst | 14 +- scripts/make-release.sh | 1 + src-ui/package-lock.json | 8 + src-ui/package.json | 1 + src-ui/src/app/app.module.ts | 7 +- .../app-frame/app-frame.component.html | 7 +- .../app-frame/app-frame.component.ts | 3 + .../common/input/select/select.component.html | 18 +- .../common/input/select/select.component.scss | 1 + .../common/input/tags/tags.component.html | 45 ++-- .../common/input/tags/tags.component.scss | 16 +- .../common/input/tags/tags.component.ts | 23 +- .../saved-view-widget.component.html | 2 +- .../document-detail.component.html | 8 +- .../document-detail.component.ts | 6 +- .../document-card-large.component.scss | 1 + .../document-card-small.component.html | 4 +- .../document-list.component.html | 6 +- .../document-list.component.scss | 24 ++- .../document-list/document-list.component.ts | 1 + .../save-view-config-dialog.component.ts | 15 +- .../filter-dropdown-date.component.html | 51 ++--- .../filter-dropdown-date.component.ts | 136 ++++++------ .../filter-dropdown.component.html | 12 +- .../filter-dropdown.component.scss | 6 + .../filter-dropdown.component.ts | 2 +- .../filter-editor.component.html | 45 ++-- .../filter-editor/filter-editor.component.ts | 64 +++--- .../correspondent-list.component.html | 23 +- .../correspondent-list.component.ts | 14 +- .../document-type-list.component.html | 21 +- .../document-type-list.component.ts | 14 +- .../generic-list/generic-list.component.ts | 2 +- .../manage/settings/settings.component.ts | 20 +- .../manage/tag-list/tag-list.component.html | 23 +- .../manage/tag-list/tag-list.component.ts | 15 +- .../src/app/interceptors/csrf.interceptor.ts | 9 +- src-ui/src/app/pipes/document-title.pipe.ts | 2 +- .../services/document-list-view.service.ts | 6 +- src-ui/src/assets/save-filter.png | Bin 8267 -> 8263 bytes src-ui/src/environments/environment.prod.ts | 3 +- src-ui/src/styles.scss | 42 +++- src/documents/admin.py | 2 +- src/documents/checks.py | 3 +- src/documents/file_handling.py | 7 +- .../management/commands/decrypt_documents.py | 13 +- src/documents/migrations/1003_mime_types.py | 17 +- .../migrations/1008_auto_20201216_1736.py | 34 +++ .../migrations/1009_auto_20201216_2005.py | 29 +++ src/documents/models.py | 15 +- src/documents/parsers.py | 6 +- src/documents/serialisers.py | 8 +- src/documents/templates/index.html | 1 + src/documents/tests/test_api.py | 203 ++++++++---------- src/documents/tests/test_file_handling.py | 11 +- src/documents/tests/test_index.py | 21 ++ src/documents/tests/test_sanity_check.py | 10 +- src/documents/tests/utils.py | 3 +- src/documents/views.py | 19 +- src/paperless/version.py | 2 +- src/paperless_mail/mail.py | 10 +- src/paperless_mail/tests/test_mail.py | 4 +- src/paperless_text/parsers.py | 62 +----- src/paperless_text/tests/samples/test.txt | 1 + src/paperless_text/tests/test_parser.py | 26 +++ 70 files changed, 874 insertions(+), 473 deletions(-) create mode 100644 src/documents/migrations/1008_auto_20201216_1736.py create mode 100644 src/documents/migrations/1009_auto_20201216_2005.py create mode 100644 src/paperless_text/tests/samples/test.txt create mode 100644 src/paperless_text/tests/test_parser.py diff --git a/README.md b/README.md index 41f85af19..e8ae8feb2 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [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-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, see below. +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. This project is still in development and some things may not work as expected. @@ -15,11 +15,13 @@ This project is still in development and some things may not work as expected. Paperless does not control your scanner, it only helps you deal with what your scanner produces. -1. Buy a document scanner that can write to a place on your network. If you need some inspiration, have a look at the [scanner recommendations](https://paperless-ng.readthedocs.io/en/latest/scanners.html) page. -2. Set it up to "scan to FTP" or something similar. It should be able to push scanned images to a server without you having to do anything. Of course if your scanner doesn't know how to automatically upload the file somewhere, you can always do that manually. Paperless doesn't care how the documents get into its local consumption directory. -3. Have the target server run the Paperless consumption script to OCR the file and index it into a local database. -4. Use the web frontend to sift through the database and find what you want. -5. Download the PDF you need/want via the web interface and do whatever you like with it. You can even print it and send it as if it's the original. In most cases, no one will care or notice. +1. Buy a document scanner that can write to a place on your network. If you need some inspiration, have a look at the [scanner recommendations](https://paperless-ng.readthedocs.io/en/latest/scanners.html) page. Set it up to "scan to FTP" or something similar. It should be able to push scanned images to a server without you having to do anything. Of course if your scanner doesn't know how to automatically upload the file somewhere, you can always do that manually. Paperless doesn't care how the documents get into its local consumption directory. + + - Alternatively, you can use any of the mobile scanning apps out there. We have an app that allows you to share documents with paperless, if you're on Android. See the section on affiliated projects. + +2. Wait for paperless to process your files. OCR is expensive, and depending on the power of your machine, this might take a bit of time. +3. Use the web frontend to sift through the database and find what you want. +4. Download the PDF you need/want via the web interface and do whatever you like with it. You can even print it and send it as if it's the original. In most cases, no one will care or notice. Here's what you get: @@ -39,7 +41,6 @@ Here's what you get: * When adding documents from mails, paperless can move these mails to a new folder, mark them as read, flag them or delete them. * Machine learning powered document matching. * Paperless learns from your documents and will be able to automatically assign tags, correspondents and types to documents once you've stored a few documents in paperless. -* We have a mobile app that offers a 'Share with paperless' option over at https://github.com/qcasey/paperless_share. You can use that in combination with any of the mobile scanning apps out there. It's still a little rough around the edges, but it works! * 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. @@ -78,7 +79,7 @@ The recommended way to deploy paperless is docker-compose. Don't clone the repos Read the [documentation](https://paperless-ng.readthedocs.io/en/latest/setup.html#installation) on how to get started. -Alternatively, you can install the dependencies and setup apache and a database server yourself. The documenation has information about the individual components of paperless that you need to take care of. +Alternatively, you can install the dependencies and setup apache and a database server yourself. The documenation has a step by step guide on how to do it. # Migrating to paperless-ng @@ -102,13 +103,15 @@ If you want to implement something big: Please start a discussion about that in Paperless has been around a while now, and people are starting to build stuff on top of it. If you're one of those people, we can add your project to this list: -* [Paperless App](https://github.com/bauerj/paperless_app): An Android/iOS app for Paperless. We're working on making this compatible. +* [Paperless App](https://github.com/bauerj/paperless_app): An Android/iOS app for Paperless. Updated to work with paperless-ng. +* [Paperless Share](https://github.com/qcasey/paperless_share). Share any files from your Android application with paperless. Very simple, but works with all of the mobile scanning apps out there that allow you to share scanned documents. + +These projects also exist, but their status and compatibility with paperless-ng is unknown. + * [Paperless Desktop](https://github.com/thomasbrueggemann/paperless-desktop): A desktop UI for your Paperless installation. Runs on Mac, Linux, and Windows. * [ansible-role-paperless](https://github.com/ovv/ansible-role-paperless): An easy way to get Paperless running via Ansible. * [paperless-cli](https://github.com/stgarf/paperless-cli): A golang command line binary to interact with a Paperless instance. -Compatibility with Paperless-ng is unknown. - # Important Note Document scanners are typically used to scan sensitive documents. Things like your social insurance number, tax records, invoices, etc. Everything is stored in the clear without encryption by default (it needs to be searchable, so if someone has ideas on how to do that on encrypted data, I'm all ears). This means that Paperless should never be run on an untrusted host. Instead, I recommend that if you do want to use it, run it locally on a server in your own home. diff --git a/docker/hub/docker-compose.postgres.yml b/docker/hub/docker-compose.postgres.yml index 24f0e118f..d33e4c38d 100644 --- a/docker/hub/docker-compose.postgres.yml +++ b/docker/hub/docker-compose.postgres.yml @@ -15,7 +15,7 @@ services: POSTGRES_PASSWORD: paperless webserver: - image: jonaswinkler/paperless-ng:0.9.6 + image: jonaswinkler/paperless-ng:0.9.8 restart: always depends_on: - db diff --git a/docker/hub/docker-compose.sqlite.yml b/docker/hub/docker-compose.sqlite.yml index 6ae619fd6..c130dfef6 100644 --- a/docker/hub/docker-compose.sqlite.yml +++ b/docker/hub/docker-compose.sqlite.yml @@ -5,7 +5,7 @@ services: restart: always webserver: - image: jonaswinkler/paperless-ng:0.9.6 + image: jonaswinkler/paperless-ng:0.9.8 restart: always depends_on: - broker diff --git a/docs/advanced_usage.rst b/docs/advanced_usage.rst index b5ae254b3..48a86384c 100644 --- a/docs/advanced_usage.rst +++ b/docs/advanced_usage.rst @@ -263,10 +263,10 @@ using the identifier which it has assigned to each document. You will end up get files like ``0000123.pdf`` in your media directory. This isn't necessarily a bad thing, because you normally don't have to access these files manually. However, if you wish to name your files differently, you can do that by adjusting the -``PAPERLESS_FILENAME_FORMAT`` settings variable. +``PAPERLESS_FILENAME_FORMAT`` configuration option. -This variable allows you to configure the filename (folders are allowed!) using -placeholders. For example, setting +This variable allows you to configure the filename (folders are allowed) using +placeholders. For example, configuring this to .. code:: bash @@ -277,17 +277,16 @@ will create a directory structure as follows: .. code:: 2019/ - my_bank/ - statement-january-0000001.pdf - statement-february-0000002.pdf + My bank/ + Statement January.pdf + Statement February.pdf 2020/ - my_bank/ - statement-january-0000003.pdf - shoe_store/ - my_new_shoes-0000004.pdf - -Paperless appends the unique identifier of each document to the filename. This -avoids filename clashes. + My bank/ + Statement January.pdf + Letter.pdf + Letter_01.pdf + Shoe store/ + My new shoes.pdf .. danger:: @@ -299,6 +298,7 @@ Paperless provides the following placeholders withing filenames: * ``{correspondent}``: The name of the correspondent, or "none". * ``{document_type}``: The name of the document type, or "none". +* ``{tag_list}``: A comma separated list of all tags assigned to the document. * ``{title}``: The title of the document. * ``{created}``: The full date and time the document was created. * ``{created_year}``: Year created only. @@ -309,8 +309,14 @@ Paperless provides the following placeholders withing filenames: * ``{added_month}``: Month added only (number 1-12). * ``{added_day}``: Day added only (number 1-31). -Paperless will convert all values for the placeholders into values which are safe -for use in filenames. + +Paperless will try to conserve the information from your database as much as possible. +However, some characters that you can use in document titles and correspondent names (such +as ``: \ /`` and a couple more) are not allowed in filenames and will be replaced with dashes. + +If paperless detects that two documents share the same filename, paperless will automatically +append ``_01``, ``_02``, etc to the filename. This happens if all the placeholders in a filename +evaluate to the same value. .. hint:: diff --git a/docs/changelog.rst b/docs/changelog.rst index a50fc31d5..a993eb530 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,58 @@ Changelog ********* + +paperless-ng 0.9.8 +################## + +This release addresses two severe issues with the previous release. + +* The delete buttons for document types, correspondents and tags were not working. +* The document section in the admin was causing internal server errors (500). + + +paperless-ng 0.9.7 +################## + + +* Front end + + * Thanks to the hard work of `Michael Shamoon`_, paperless now comes with a much more streamlined UI for + filtering documents. + + * `Michael Shamoon`_ replaced the document preview with another component. This should fix compatibility with Safari browsers. + + * Added buttons to the management pages to quickly show all documents with one specific tag, correspondent, or title. + + * Paperless now stores your saved views on the server and associates them with your user account. + This means that you can access your views on multiple devices and have separate views for different users. + You will have to recreate your views. + + * The GitHub and documentation links now open in new tabs/windows. Thanks to `rYR79435`_. + + * Paperless now generates default saved view names when saving views with certain filter rules. + + * Added a small version indicator to the front end. + +* Other additions and changes + + * The new filename format field ``{tag_list}`` inserts a list of tags into the filename, separated by comma. + * The ``document_retagger`` no longer removes inbox tags or tags without matching rules. + * The new configuration option ``PAPERLESS_COOKIE_PREFIX`` allows you to run multiple instances of paperless on different ports. + This option enables you to be logged in into multiple instances by specifying different cookie names for each instance. + +* Fixes + + * Sometimes paperless would assign dates in the future to newly consumed documents. + * The filename format fields ``{created_month}`` and ``{created_day}`` now use a leading zero for single digit values. + * The filename format field ``{tags}`` can no longer be used without arguments. + * Paperless was not able to consume many images (especially images from mobile scanners) due to missing DPI information. + Paperless now assumes A4 paper size for PDF generation if no DPI information is present. + * Documents with empty titles could not be opened from the table view due to the link being empty. + * Fixed an issue with filenames containing special characters such as ``:`` not being accepted for upload. + * Fixed issues with thumbnail generation for plain text files. + + paperless-ng 0.9.6 ################## @@ -841,6 +893,8 @@ bulk of the work on this big change. * Initial release +.. _rYR79435: https://github.com/rYR79435 +.. _Michael Shamoon: https://github.com/shamoon .. _jayme-github: http://github.com/jayme-github .. _Brian Conn: https://github.com/TheConnMan .. _Christopher Luu: https://github.com/nuudles diff --git a/docs/usage_overview.rst b/docs/usage_overview.rst index bb9ecd452..7a4fd7740 100644 --- a/docs/usage_overview.rst +++ b/docs/usage_overview.rst @@ -57,9 +57,6 @@ Adding documents to paperless ############################# Once you've got Paperless setup, you need to start feeding documents into it. -Currently, there are four options: the consumption directory, the dashboard, IMAP (email), and -HTTP POST. - When adding documents to paperless, it will perform the following operations on your documents: @@ -112,6 +109,17 @@ Dashboard upload The dashboard has a file drop field to upload documents to paperless. Simply drag a file onto this field or select a file with the file dialog. Multiple files are supported. + +Mobile upload +============= + +The mobile app over at ``_ allows Android users +to share any documents with paperless. This can be combined with any of the mobile +scanning apps out there, such as Office Lens. + +Furthermore, there is the `Paperless App `_ as well, +which no only has document upload, but also document editing and browsing. + .. _usage-email: IMAP (Email) diff --git a/scripts/make-release.sh b/scripts/make-release.sh index 0a7bc7a9b..f5c9028fa 100755 --- a/scripts/make-release.sh +++ b/scripts/make-release.sh @@ -5,6 +5,7 @@ # adjust src/paperless/version.py # changelog in the documentation # adjust versions in docker/hub/* +# adjust version in src-ui/src/environments/prod # If docker-compose was modified: all compose files are the same. # Steps: diff --git a/src-ui/package-lock.json b/src-ui/package-lock.json index 5eca0b3c0..10215a32d 100644 --- a/src-ui/package-lock.json +++ b/src-ui/package-lock.json @@ -2056,6 +2056,14 @@ "tslib": "^2.0.0" } }, + "@ng-select/ng-select": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-5.0.9.tgz", + "integrity": "sha512-YZeSAiS8/Nx/eHZJPmOOYL8YmcvSq+dr1P8WIrsKmRA7mueorBpPc5xlUj+nLQbpLtsiQvdWDQspf/ykOvD/lA==", + "requires": { + "tslib": "^2.0.0" + } + }, "@ngtools/webpack": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-10.2.0.tgz", diff --git a/src-ui/package.json b/src-ui/package.json index 6293f2672..14d828483 100644 --- a/src-ui/package.json +++ b/src-ui/package.json @@ -21,6 +21,7 @@ "@angular/platform-browser-dynamic": "~10.1.5", "@angular/router": "~10.1.5", "@ng-bootstrap/ng-bootstrap": "^8.0.0", + "@ng-select/ng-select": "^5.0.9", "bootstrap": "^4.5.0", "ng-bootstrap": "^1.6.3", "ng2-pdf-viewer": "^6.3.2", diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index 627d4f6cf..6c4cabe92 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -56,6 +56,7 @@ import { FilterPipe } from './pipes/filter.pipe'; import { DocumentTitlePipe } from './pipes/document-title.pipe'; import { MetadataCollapseComponent } from './components/document-detail/metadata-collapse/metadata-collapse.component'; import { SelectDialogComponent } from './components/common/select-dialog/select-dialog.component'; +import { NgSelectModule } from '@ng-select/ng-select'; @NgModule({ declarations: [ @@ -114,7 +115,8 @@ import { SelectDialogComponent } from './components/common/select-dialog/select- ReactiveFormsModule, NgxFileDropModule, InfiniteScrollModule, - PdfViewerModule + PdfViewerModule, + NgSelectModule ], providers: [ DatePipe, @@ -123,7 +125,8 @@ import { SelectDialogComponent } from './components/common/select-dialog/select- useClass: CsrfInterceptor, multi: true }, - FilterPipe + FilterPipe, + DocumentTitlePipe ], bootstrap: [AppComponent] }) diff --git a/src-ui/src/app/components/app-frame/app-frame.component.html b/src-ui/src/app/components/app-frame/app-frame.component.html index 7876150af..2458005f4 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.html +++ b/src-ui/src/app/components/app-frame/app-frame.component.html @@ -17,6 +17,11 @@