mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-08-10 00:18:57 +00:00
Compare commits
37 Commits
v2.7.2
...
feature-rt
Author | SHA1 | Date | |
---|---|---|---|
![]() |
292d7762c4 | ||
![]() |
63e1f9f5d3 | ||
![]() |
bd4476d484 | ||
![]() |
7a0334f353 | ||
![]() |
d03e48ea88 | ||
![]() |
342e6d4679 | ||
![]() |
584f1361ad | ||
![]() |
05b1ff9738 | ||
![]() |
d65fcf70f3 | ||
![]() |
a5d3d51cc5 | ||
![]() |
f4489ca2e7 | ||
![]() |
e40893e74f | ||
![]() |
d002ae2e05 | ||
![]() |
bf430865b4 | ||
![]() |
a47d36f5e5 | ||
![]() |
4392628bd7 | ||
![]() |
6d25eb26a1 | ||
![]() |
95fd1ae879 | ||
![]() |
78f338484f | ||
![]() |
40db1065dc | ||
![]() |
b720aa3cd1 | ||
![]() |
e837f1e85b | ||
![]() |
ea2012bc81 | ||
![]() |
8e39315586 | ||
![]() |
f009d9868e | ||
![]() |
1bbcd0961b | ||
![]() |
4fa2b54aed | ||
![]() |
7281c110c6 | ||
![]() |
f812f2af4d | ||
![]() |
47b4a602a7 | ||
![]() |
ca73c0d1f3 | ||
![]() |
7f6a50be5b | ||
![]() |
10e10f9ff4 | ||
![]() |
95c24a50f7 | ||
![]() |
d06faa2fcb | ||
![]() |
bed66cced0 | ||
![]() |
ceaf60e6ad |
@@ -5,7 +5,7 @@
|
||||
repos:
|
||||
# General hooks
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: check-docstring-first
|
||||
- id: check-json
|
||||
@@ -47,11 +47,11 @@ repos:
|
||||
exclude: "(^Pipfile\\.lock$)"
|
||||
# Python hooks
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: 'v0.3.5'
|
||||
rev: 'v0.4.1'
|
||||
hooks:
|
||||
- id: ruff
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 24.3.0
|
||||
rev: 24.4.0
|
||||
hooks:
|
||||
- id: black
|
||||
# Dockerfile hooks
|
||||
|
@@ -52,7 +52,7 @@ ARG TARGETARCH
|
||||
|
||||
# Can be workflow provided, defaults set for manual building
|
||||
ARG JBIG2ENC_VERSION=0.29
|
||||
ARG QPDF_VERSION=11.6.4
|
||||
ARG QPDF_VERSION=11.9.0
|
||||
ARG GS_VERSION=10.02.1
|
||||
|
||||
# Set Python environment variables
|
||||
|
4
Pipfile
4
Pipfile
@@ -14,7 +14,7 @@ django-celery-results = "*"
|
||||
django-compression-middleware = "*"
|
||||
django-cors-headers = "*"
|
||||
django-extensions = "*"
|
||||
django-filter = "~=24.1"
|
||||
django-filter = "~=24.2"
|
||||
django-guardian = "*"
|
||||
django-multiselectfield = "*"
|
||||
djangorestframework = "==3.14.0"
|
||||
@@ -22,7 +22,7 @@ djangorestframework-guardian = "*"
|
||||
drf-writable-nested = "*"
|
||||
bleach = "*"
|
||||
celery = {extras = ["redis"], version = "*"}
|
||||
channels = "~=4.0"
|
||||
channels = "~=4.1"
|
||||
channels-redis = "*"
|
||||
concurrent-log-handler = "*"
|
||||
filelock = "*"
|
||||
|
760
Pipfile.lock
generated
760
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "81e90e43e2782425b1f3edb9fab7b5ddf9a81b5adc211b1102d9a584e9dc000d"
|
||||
"sha256": "e1ac865502fd8502e8534557675cc36133252871db228d97a3c0c151891600f3"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
@@ -230,12 +230,12 @@
|
||||
},
|
||||
"channels": {
|
||||
"hashes": [
|
||||
"sha256:0ce53507a7da7b148eaa454526e0e05f7da5e5d1c23440e4886cf146981d8420",
|
||||
"sha256:2253334ac76f67cba68c2072273f7e0e67dbdac77eeb7e318f511d2f9a53c5e4"
|
||||
"sha256:a3c4419307f582c3f71d67bfb6eff748ae819c2f360b9b141694d84f242baa48",
|
||||
"sha256:e0ed375719f5c1851861f05ed4ce78b0166f9245ca0ecd836cb77d4bb531489d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==4.0.0"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==4.1.0"
|
||||
},
|
||||
"channels-redis": {
|
||||
"hashes": [
|
||||
@@ -479,12 +479,12 @@
|
||||
},
|
||||
"django-auditlog": {
|
||||
"hashes": [
|
||||
"sha256:7bc2c87e4aff62dec9785d1b2359a2b27148f8c286f8a52b9114fc7876c5a9f7",
|
||||
"sha256:b9d3acebb64f3f2785157efe3f2f802e0929aafc579d85bbfb9827db4adab532"
|
||||
"sha256:92db1cf4a51ceca5c26b3ff46997d9e3305a02da1bd435e2efb5b8b6d300ce1f",
|
||||
"sha256:9de49f80a4911135d136017123cd73461f869b4947eec14d5e76db4b88182f3f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.3.0"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==3.0.0"
|
||||
},
|
||||
"django-celery-results": {
|
||||
"hashes": [
|
||||
@@ -522,12 +522,12 @@
|
||||
},
|
||||
"django-filter": {
|
||||
"hashes": [
|
||||
"sha256:335bcae6cbd3e984b024841070f567b22faea57594f27d37c52f8f131f8d8621",
|
||||
"sha256:65cb43ce272077e5ac6aae1054d76c121cd6b552e296a82a13921e9371baf8c1"
|
||||
"sha256:48e5fc1da3ccd6ca0d5f9bb550973518ce977a4edde9d2a8a154a7f4f0b9f96e",
|
||||
"sha256:df2ee9857e18d38bed203c8745f62a803fa0f31688c9fe6f8e868120b1848e48"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==24.1"
|
||||
"version": "==24.2"
|
||||
},
|
||||
"django-guardian": {
|
||||
"hashes": [
|
||||
@@ -581,12 +581,12 @@
|
||||
},
|
||||
"filelock": {
|
||||
"hashes": [
|
||||
"sha256:5ffa845303983e7a0b7ae17636509bc97997d58afeafa72fb141a17b152284cb",
|
||||
"sha256:a79895a25bbefdf55d1a2a0a80968f7dbb28edcd6d4234a0afb3f37ecde4b546"
|
||||
"sha256:404e5e9253aa60ad457cae1be07c0f0ca90a63931200a47d9b6a6af84fd7b45f",
|
||||
"sha256:d13f466618bfde72bd2c18255e269f72542c6e70e7bac83a0232d6b1cc5c8cf4"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==3.13.3"
|
||||
"version": "==3.13.4"
|
||||
},
|
||||
"flower": {
|
||||
"hashes": [
|
||||
@@ -608,12 +608,12 @@
|
||||
},
|
||||
"gunicorn": {
|
||||
"hashes": [
|
||||
"sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0",
|
||||
"sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033"
|
||||
"sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9",
|
||||
"sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==21.2.0"
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==22.0.0"
|
||||
},
|
||||
"h11": {
|
||||
"hashes": [
|
||||
@@ -830,19 +830,20 @@
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca",
|
||||
"sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"
|
||||
"sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc",
|
||||
"sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==3.6"
|
||||
"version": "==3.7"
|
||||
},
|
||||
"imap-tools": {
|
||||
"hashes": [
|
||||
"sha256:b6c2b94b9d168e1a52c419a2c10367d746694ca61b68fdb0c3ff211046a0760d",
|
||||
"sha256:f42dfb1a7db666ef5df4c815e5b6e6571707b188f67c0a411fc5c9a9b4f1b85f"
|
||||
"sha256:1fc97a06a35e18a6d0e775d40b18198f52dae0fa46ee8b6fc6f82dbc7a701699",
|
||||
"sha256:6f6a354a6bb95ab83a24d3321d1a8b62b299ba76ba5774e9f45f0ad2544f093b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.5.0"
|
||||
"version": "==1.6.0"
|
||||
},
|
||||
"img2pdf": {
|
||||
"hashes": [
|
||||
@@ -868,11 +869,11 @@
|
||||
},
|
||||
"joblib": {
|
||||
"hashes": [
|
||||
"sha256:92f865e621e17784e7955080b6d042489e3b8e294949cc44c6eac304f59772b1",
|
||||
"sha256:ef4331c65f239985f3f2220ecc87db222f08fd22097a3dd5698f693875f8cbb9"
|
||||
"sha256:1eb0dc091919cd384490de890cb5dfd538410a6d4b3b54eef09fb8c50b409b1c",
|
||||
"sha256:42942470d4062537be4d54c83511186da1fc14ba354961a2114da91efa9a4ed7"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.3.2"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.4.0"
|
||||
},
|
||||
"kombu": {
|
||||
"hashes": [
|
||||
@@ -1406,12 +1407,12 @@
|
||||
},
|
||||
"python-ipware": {
|
||||
"hashes": [
|
||||
"sha256:3a94fd073b93e12b13617e291f13eda3495d3ba68b580e3e30174ea84ac63041",
|
||||
"sha256:86e30cc3af62cec42284dedd49c8a14e436e73c96433c8645ec0b476ff4ad7ec"
|
||||
"sha256:9117b1c4dddcb5d5ca49e6a9617de2fc66aec2ef35394563ac4eecabdf58c062",
|
||||
"sha256:fc936e6e7ec9fcc107f9315df40658f468ac72f739482a707181742882e36b60"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.0.2"
|
||||
"version": "==3.0.0"
|
||||
},
|
||||
"python-magic": {
|
||||
"hashes": [
|
||||
@@ -1503,100 +1504,100 @@
|
||||
},
|
||||
"rapidfuzz": {
|
||||
"hashes": [
|
||||
"sha256:01581b688c5f4f6665b779135e32db0edab1d78028abf914bb91469928efa383",
|
||||
"sha256:04bae4d9c16ce1bab6447d196fb8258d98139ed8f9b288a38b84887985e4227b",
|
||||
"sha256:075a419a0ec29be44b3d7f4bcfa5cb7e91e419379a85fc05eb33de68315bd96f",
|
||||
"sha256:0828b55ec8ad084febdf4ab0c942eb1f81c97c0935f1cb0be0b4ea84ce755988",
|
||||
"sha256:08671280e0c04d2bb3f39511f13cae5914e6690036fd1eefc3d47a47f9fae634",
|
||||
"sha256:0b13a6823a1b83ae43f8bf35955df35032bee7bec0daf9b5ab836e0286067434",
|
||||
"sha256:0cc77237242303733de47829028a0a8b6ab9188b23ec9d9ff0a674fdcd3c8e7f",
|
||||
"sha256:1252ca156e1b053e84e5ae1c8e9e062ee80468faf23aa5c543708212a42795fd",
|
||||
"sha256:150c98b65faff17b917b9d36bff8a4d37b6173579c6bc2e38ff2044e209d37a4",
|
||||
"sha256:1522eaab91b9400b3ef16eebe445940a19e70035b5bc5d98aef23d66e9ac1df0",
|
||||
"sha256:16895dc62a7b92028f9c8b6d22830f1cbc77306ee794f461afc6028e1a8d7539",
|
||||
"sha256:187db4cc8fb54f8c49c67b7f38ef3a122ce23be273032fa2ff34112a2694c3d8",
|
||||
"sha256:18bc2f13c73d5d34499ff6ada55b052c445d3aa64d22c2639e5ab45472568046",
|
||||
"sha256:1dfceaa7c2914585bb8a043265c39ec09078f13fbf53b5525722fc074306b6fa",
|
||||
"sha256:1efa2268b51b68156fb84d18ca1720311698a58051c4a19c40d670057ce60519",
|
||||
"sha256:209dda6ae66b702f74a78cef555397cdc2a83d7f48771774a20d2fc30808b28c",
|
||||
"sha256:20e7d729af2e5abb29caa070ec048aba042f134091923d9ca2ac662b5604577e",
|
||||
"sha256:2bc0b78572626af6ab134895e4dbfe4f4d615d18dcc43b8d902d8e45471aabba",
|
||||
"sha256:2f9070b42c0ba030b045bba16a35bdb498a0d6acb0bdb3ff4e325960e685e290",
|
||||
"sha256:358692f1df3f8aebcd48e69c77c948c9283b44c0efbaf1eeea01739efe3cd9a6",
|
||||
"sha256:3a6a36c9299e059e0bee3409218bc5235a46570c20fc980cdee5ed21ea6110ad",
|
||||
"sha256:3e55f02105c451ab6ff0edaaba57cab1b6c0a0241cfb2b306d4e8e1503adba50",
|
||||
"sha256:40998c8dc35fdd221790b8b5134a8d7499adbfab9a5dd9ec626c7e92e17a43ed",
|
||||
"sha256:41851620d2900791d66d9b6092fc163441d7dd91a460c73b07957ff1c517bc30",
|
||||
"sha256:419c8961e861fb5fc5590056c66a279623d1ea27809baea17e00cdc313f1217a",
|
||||
"sha256:42c2e8a2341363c7caf276efdbe1a673fc5267a02568c47c8e980f12e9bc8727",
|
||||
"sha256:4604dfc1098920c4eb6d0c6b5cc7bdd4bf95b48633e790c1d3f100a25870691d",
|
||||
"sha256:49b0c47860c733a3d73a4b70b97b35c8cbf24ef24f8743732f0d1c412a8c85de",
|
||||
"sha256:4bb9285abeb0477cdb2f8ea0cf7fd4b5f72ed5a9a7d3f0c0bb4a5239db2fc1ed",
|
||||
"sha256:4e09d81008e212fc824ea23603ff5270d75886e72372fa6c7c41c1880bcb57ed",
|
||||
"sha256:4e50840a8a8e0229563eeaf22e21a203359859557db8829f4d0285c17126c5fb",
|
||||
"sha256:4efa9bfc5b955b6474ee077eee154e240441842fa304f280b06e6b6aa58a1d1e",
|
||||
"sha256:51a5b96d2081c3afbef1842a61d63e55d0a5a201473e6975a80190ff2d6f22ca",
|
||||
"sha256:579cce49dfa57ffd8c8227b3fb53cced54b4df70cec502e63e9799b4d1f44004",
|
||||
"sha256:594b9c33fc1a86784962043ee3fbaaed875fbaadff72e467c2f7a83cd6c5d69d",
|
||||
"sha256:5a8ba64d72329a940ff6c74b721268c2004eecc48558f648a38e96915b5d1c1b",
|
||||
"sha256:5bd394e28ff221557ea4d8152fcec3e66d9f620557feca5f2bedc4c21f8cf2f9",
|
||||
"sha256:5f2075ac9ee5c15d33d24a1efc8368d095602b5fd9634c5b5f24d83e41903528",
|
||||
"sha256:600b4d4315f33ec0356c0dab3991a5d5761102420bcff29e0773706aa48936e8",
|
||||
"sha256:611278ce3136f4544d596af18ab8849827d64372e1d8888d9a8d071bf4a3f44d",
|
||||
"sha256:620df112c39c6d27316dc1e22046dc0382d6d91fd60d7c51bd41ca0333d867e9",
|
||||
"sha256:63044c63565f50818d885bfcd40ac369947da4197de56b4d6c26408989d48edf",
|
||||
"sha256:632f09e19365ace5ff2670008adc8bf23d03d668b03a30230e5b60ff9317ee93",
|
||||
"sha256:74e692357dd324dff691d379ef2c094c9ec526c0ce83ed43a066e4e68fe70bf6",
|
||||
"sha256:7ba14850cc8258b3764ea16b8a4409ac2ba16d229bde7a5f495dd479cd9ccd56",
|
||||
"sha256:7bc944d7e830cfce0f8b4813875f05904207017b66e25ab7ee757507001310a9",
|
||||
"sha256:7be5f460ff42d7d27729115bfe8a02e83fa0284536d8630ee900d17b75c29e65",
|
||||
"sha256:7c837f89d86a5affe9ee6574dad6b195475676a6ab171a67920fc99966f2ab2c",
|
||||
"sha256:7e4eea225d2bff1aff4c85fcc44716596d3699374d99eb5906b7a7560297460e",
|
||||
"sha256:7f9f3dc14fadbd553975f824ac48c381f42192cec9d7e5711b528357662a8d8e",
|
||||
"sha256:860f438238f1807532aa5c5c25e74c284232ccc115fe84697b78e25d48f364f7",
|
||||
"sha256:86c7676a32d7524e40bc73546e511a408bc831ae5b163029d325ea3a2027d089",
|
||||
"sha256:86eea3e6c314a9238de568254a9c591ec73c2985f125675ed5f171d869c47773",
|
||||
"sha256:8e11c5e6593be41a555475c9c20320342c1f5585d635a064924956944c465ad4",
|
||||
"sha256:8e70f876ca89a6df344f8157ac60384e8c05a0dfb442da2490c3f1c45238ccf5",
|
||||
"sha256:8fdc26e7863e0f63c2185d53bb61f5173ad4451c1c8287b535b30ea25a419a5a",
|
||||
"sha256:91f798cc00cd94a0def43e9befc6e867c9bd8fa8f882d1eaa40042f528b7e2c7",
|
||||
"sha256:92b8146fbfb37ac358ef7e0f6b79619e4f793fbbe894b99ea87920f9c0a9d77d",
|
||||
"sha256:9b6167468f76779a14b9af66210f68741af94d32d086f19118de4e919f00585c",
|
||||
"sha256:9bad6a0fe3bc1753dacaa6229a8ba7d9844eb7ae24d44d17c5f4c51c91a8a95e",
|
||||
"sha256:9e17a3092e74025d896ef1d67ac236c83494da37a78ef84c712e4e2273c115f1",
|
||||
"sha256:9ea720db8def684c1eb71dadad1f61c9b52f4d979263eb5d443f2b22b0d5430a",
|
||||
"sha256:a1f268a2a37cd22573b4a06eccd481c04504b246d3cadc2d8e8dfa64b575636d",
|
||||
"sha256:a9460d8fddac7ea46dff9298eee9aa950dbfe79f2eb509a9f18fbaefcd10894c",
|
||||
"sha256:a9acca34b34fb895ee6a84c436bb919f3b9cd8f43e7003d43e9573a1d990ff74",
|
||||
"sha256:aa163257a0ac4e70f9009d25e5030bdd83a8541dfa3ba78dc86b35c9e16a80b4",
|
||||
"sha256:b4a7e37fe136022d944374fcd8a2f72b8a19f7b648d2cdfb946667e9ede97f9f",
|
||||
"sha256:b5881856f830351aaabd869151124f64a80bf61560546d9588a630a4e933a5de",
|
||||
"sha256:b917764fd2b267addc9d03a96d26f751f6117a95f617428c44a069057653b528",
|
||||
"sha256:be08f39e397a618aab907887465d7fabc2d1a4d15d1a67cb8b526a7fb5202a3e",
|
||||
"sha256:c0cc9d3c8261457af3f8756b1f71a9fdc4892978a9e8b967976d2803e08bf972",
|
||||
"sha256:c788b11565cc176fab8fab6dfcd469031e906927db94bf7e422afd8ef8f88a5a",
|
||||
"sha256:c86bc4b1d2380739e6485396195e30021df509b4923f3f757914e171587bce7c",
|
||||
"sha256:cda4550a98658f9a8bcdc03d0498ed1565c1563880e3564603a9eaae28d51b2a",
|
||||
"sha256:ce728e2b582fd396bc2559160ee2e391e6a4b5d2e455624044699d96abe8a396",
|
||||
"sha256:d1b14489b038f007f425a06fcf28ac6313c02cb603b54e3a28d9cfae82198cc0",
|
||||
"sha256:d5a3872f35bec89f07b993fa1c5401d11b9e68bcdc1b9737494e279308a38a5f",
|
||||
"sha256:d7361608c8e73a1dc0203a87d151cddebdade0098a047c46da43c469c07df964",
|
||||
"sha256:d7878025248b99ccca3285891899373f98548f2ca13835d83619ffc42241c626",
|
||||
"sha256:dc3fdb4738a6b83ae27f1d8923b00d3a9c2b5c50da75b9f8b81841839c6e3e1f",
|
||||
"sha256:dd5ad2c12dab2b98340c4b7b9592c8f349730bda9a2e49675ea592bbcbc1360b",
|
||||
"sha256:dfd1e4819f1f3c47141f86159b44b7360ecb19bf675080b3b40437bf97273ab9",
|
||||
"sha256:e499c823206c9ffd9d89aa11f813a4babdb9219417d4efe4c8a6f8272da00e98",
|
||||
"sha256:e8041c6b2d339766efe6298fa272f79d6dd799965df364ef4e50f488c101c899",
|
||||
"sha256:eace9fdde58a425d4c9a93021b24a0cac830df167a5b2fc73299e2acf9f41493",
|
||||
"sha256:ecd70212fd9f1f8b1d3bdd8bcb05acc143defebd41148bdab43e573b043bb241",
|
||||
"sha256:ef6b6ab64c4c91c57a6b58e1d690b59453bfa1f1e9757a7e52e59b4079e36631",
|
||||
"sha256:f332d61f51b0b9c8b55a0fb052b4764b6ad599ea8ce948ac47a4388e9083c35e",
|
||||
"sha256:f39eb1513ee139ba6b5c01fe47ddf2d87e9560dd7fdee1068f7f6efbae70de34",
|
||||
"sha256:faded69ffe79adcefa8da08f414a0fd52375e2b47f57be79471691dad9656b5a"
|
||||
"sha256:00b5ee47b387fa3805f4038362a085ec58149135dc5bc640ca315a9893a16f9e",
|
||||
"sha256:0798e32304b8009d215026bf7e1c448f1831da0a03987b7de30059a41bee92f3",
|
||||
"sha256:07d7d4a3c49a15146d65f06e44d7545628ca0437c929684e32ef122852f44d95",
|
||||
"sha256:14791324f0c753f5a0918df1249b91515f5ddc16281fbaa5ec48bff8fa659229",
|
||||
"sha256:16153a97efacadbd693ccc612a3285df2f072fd07c121f30c2c135a709537075",
|
||||
"sha256:17d79398849c1244f646425cf31d856eab9ebd67b7d6571273e53df724ca817e",
|
||||
"sha256:1905d9319a97bed29f21584ca641190dbc9218a556202b77876f1e37618d2e03",
|
||||
"sha256:1b176f01490b48337183da5b4223005bc0c2354a4faee5118917d2fba0bedc1c",
|
||||
"sha256:1c0264d03dcee1bb975975b77c2fe041820fb4d4a25a99e3cb74ddd083d671ca",
|
||||
"sha256:1d5592b08e3cadc9e06ef3af6a9d66b6ef1bf871ed5acd7f9b1e162d78806a65",
|
||||
"sha256:1edafc0a2737df277d3ddf401f3a73f76e246b7502762c94a3916453ae67e9b1",
|
||||
"sha256:1ef119fc127c982053fb9ec638dcc3277f83b034b5972eb05941984b9ec4a290",
|
||||
"sha256:2084193fd8fd346db496a2220363437eb9370a06d1d5a7a9dba00a64390c6a28",
|
||||
"sha256:209bb712c448cdec4def6260b9f059bd4681ec61a01568f5e70e37bfe9efe830",
|
||||
"sha256:231dc1cb63b1c8dd78c0597aa3ad3749a86a2b7e76af295dd81609522699a558",
|
||||
"sha256:25498650e30122f4a5ad6b27c7614b4af8628c1d32b19d406410d33f77a86c80",
|
||||
"sha256:267ff42370e031195e3020fff075420c136b69dc918ecb5542ec75c1e36af81f",
|
||||
"sha256:2a8a007fdc5cf646e48e361a39eabe725b93af7673c5ab90294e551cae72ff58",
|
||||
"sha256:2ba0e43e9a94d256a704a674c7010e6f8ef9225edf7287cf3e7f66c9894b06cd",
|
||||
"sha256:2c6a43446f0cd8ff347b1fbb918dc0d657bebf484ddfa960ee069e422a477428",
|
||||
"sha256:30c282612b7ebf2d7646ebebfd98dd308c582246a94d576734e4b0162f57baf4",
|
||||
"sha256:313bdcd16e9cd5e5568b4a31d18a631f0b04cc10a3fd916e4ef75b713e6f177e",
|
||||
"sha256:392582aa784737d95255ca122ebe7dca3c774da900d100c07b53d32cd221a60e",
|
||||
"sha256:3aff3b829b0b04bdf78bd780ec9faf5f26eac3591df98c35a0ae216c925ae436",
|
||||
"sha256:3fee62ae76e3b8b9fff8aa2ca4061575ee358927ffbdb2919a8c84a98da59f78",
|
||||
"sha256:41219536634bd6f85419f38450ef080cfb519638125d805cf8626443e677dc61",
|
||||
"sha256:48b6e5a337a814aec7c6dda5d6460f947c9330860615301f35b519e16dde3c77",
|
||||
"sha256:4969fe0eb179aedacee53ca8f8f1be3c655964a6d62db30f247fee444b9c52b4",
|
||||
"sha256:4d5cd86aca3f12e73bfc70015db7e8fc44122da03aa3761138b95112e83f66e4",
|
||||
"sha256:50db3867864422bf6a6435ea65b9ac9de71ef52ed1e05d62f498cd430189eece",
|
||||
"sha256:58999b21d01dd353f49511a61937eac20c7a5b22eab87612063947081855d85f",
|
||||
"sha256:5f4174079dfe8ed1f13ece9bde7660f19f98ab17e0c0d002d90cc845c3a7e238",
|
||||
"sha256:63044a7b6791a2e945dce9d812a6886e93159deb0464984eb403617ded257f08",
|
||||
"sha256:63db612bb6da1bb9f6aa7412739f0e714b1910ec07bc675943044fe683ef192c",
|
||||
"sha256:68b185a0397aebe78bcc5d0e1efd96509d4e2f3c4a05996e5c843732f547e9ef",
|
||||
"sha256:6d4f1956fe1fc618e34ac79a6ed84fff5a6f23e41a8a476dd3e8570f0b12f02b",
|
||||
"sha256:6f34a541895627c2bc9ef7757f16f02428a08d960d33208adfb96b33338d0945",
|
||||
"sha256:6f7641992de44ec2ca54102422be44a8e3fb75b9690ccd74fff72b9ac7fc00ee",
|
||||
"sha256:6f8b62fdccc429e6643cefffd5df9c7bca65588d06e8925b78014ad9ad983bf5",
|
||||
"sha256:718ea99f84b16c4bdbf6a93e53552cdccefa18e12ff9a02c5041e621460e2e61",
|
||||
"sha256:747265f39978bbaad356f5c6b6c808f0e8f5e8994875af0119b82b4700c55387",
|
||||
"sha256:77ea62879932b32aba77ab23a9296390a67d024bf2f048dee99143be80a4ce26",
|
||||
"sha256:78a0d2a11bb3936463609777c6d6d4984a27ebb2360b58339c699899d85db036",
|
||||
"sha256:799f5f221d639d1c2ed8a2348d1edf5e22aa489b58b2cc99f5bf0c1917e2d0f2",
|
||||
"sha256:81fd28389bedab28251f0535b3c034b0e63a618efc3ff1d338c81a3da723adb3",
|
||||
"sha256:827ddf2d5d157ac3d1001b52e84c9e20366237a742946599ffc435af7fdd26d0",
|
||||
"sha256:8b76abfec195bf1ee6f9ec56c33ba5e9615ff2d0a9530a54001ed87e5a6ced3b",
|
||||
"sha256:8c40da44ca20235cda05751d6e828b6b348e7a7c5de2922fa0f9c63f564fd675",
|
||||
"sha256:8e02425bfc7ebed617323a674974b70eaecd8f07b64a7d16e0bf3e766b93e3c9",
|
||||
"sha256:8e08b01dc9369941a24d7e512b0d81bf514e7d6add1b93d8aeec3c8fa08a824e",
|
||||
"sha256:90167a48de3ed7f062058826608a80242b8561d0fb0cce2c610d741624811a61",
|
||||
"sha256:9441aca94b21f7349cdb231cd0ce9ca251b2355836e8a02bf6ccbea5b442d7a9",
|
||||
"sha256:97c13f156f14f10667e1cfc4257069b775440ce005e896c09ce3aff21c9ae665",
|
||||
"sha256:987cd277d27d14301019fdf61c17524f6127f5d364be5482228726049d8e0d10",
|
||||
"sha256:9a16ef3702cecf16056c5fd66398b7ea8622ff4e3afeb00a8db3e74427e850af",
|
||||
"sha256:9ea3d2e41d8fac71cb63ee72f75bee0ed1e9c50709d4c58587f15437761c1858",
|
||||
"sha256:a02def2eb526cc934d2125533cf2f15aa71c72ed4397afca38427ab047901e88",
|
||||
"sha256:a0643a25937fafe8d117f2907606e9940cd1cc905c66f16ece9ab93128299994",
|
||||
"sha256:a2ee3909f611cc5860cc8d9f92d039fd84241ce7360b49ea88e657181d2b45f6",
|
||||
"sha256:a357aae6791118011ad3ab4f2a4aa7bd7a487e5f9981b390e9f3c2c5137ecadf",
|
||||
"sha256:aa223c73c59cc45c12eaa9c439318084003beced0447ff92b578a890288e19eb",
|
||||
"sha256:ad4dbd06c1f579eb043b2dcfc635bc6c9fb858240a70f0abd3bed84d8ac79994",
|
||||
"sha256:b0ba20be465566264fa5580d874ccf5eabba6975dba45857e2c76e2df3359c6d",
|
||||
"sha256:b27cea618601ca5032ea98ee116ca6e0fe67be7b286bcb0b9f956d64db697472",
|
||||
"sha256:b7b9cbc60e3eb08da6d18636c62c6eb6206cd9d0c7ad73996f7a1df3fc415b27",
|
||||
"sha256:bb571dbd4cc93342be0ba632f0b8d7de4cbd9d959d76371d33716d2216090d41",
|
||||
"sha256:bbc15985c5658691f637a6b97651771147744edfad2a4be56b8a06755e3932fa",
|
||||
"sha256:bc5a1ec3bd05b55d3070d557c0cdd4412272d51b4966c79aa3e9da207bd33d65",
|
||||
"sha256:bca5acf77508d1822023a85118c2dd8d3c16abdd56d2762359a46deb14daa5e0",
|
||||
"sha256:c04ef83c9ca3162d200df36e933b3ea0327a2626cee2e01bbe55acbc004ce261",
|
||||
"sha256:c21d5c7cfa6078c79897e5e482a7e84ff927143d2f3fb020dd6edd27f5469574",
|
||||
"sha256:c22b32a57ab47afb207e8fe4bd7bb58c90f9291a63723cafd4e704742166e368",
|
||||
"sha256:c458085e067c766112f089f78ce39eab2b69ba027d7bbb11d067a0b085774367",
|
||||
"sha256:c4dbb1ebc9a811f38da33f32ed2bb5f58b149289b89eb11e384519e9ba7ca881",
|
||||
"sha256:c754ce1fab41b731259f100d5d46529a38aa2c9b683c92aeb7e96ef5b2898cd8",
|
||||
"sha256:c763d99cf087e7b2c5be0cf34ae9a0e1b031f5057d2341a0a0ed782458645b7e",
|
||||
"sha256:c9597a05d08e8103ad59ebdf29e3fbffb0d0dbf3b641f102cfbeadc3a77bde51",
|
||||
"sha256:cc4af7090a626c902c48db9b5d786c1faa0d8e141571e8a63a5350419ea575bd",
|
||||
"sha256:ceb10039e7346927cec47eaa490b34abb602b537e738ee9914bb41b8de029fbc",
|
||||
"sha256:d1a15fef1938b43468002f2d81012dbc9e7b50eb8533af202b0559c2dc7865d9",
|
||||
"sha256:d4276c7ee061db0bac54846933b40339f60085523675f917f37de24a4b3ce0ee",
|
||||
"sha256:d48657a404fab82b2754faa813a10c5ad6aa594cb1829dca168a49438b61b4ec",
|
||||
"sha256:e3f882110f2f4894942e314451773c47e8b1b4920b5ea2b6dd2e2d4079dd3135",
|
||||
"sha256:e4c647795c5b901091a68e210c76b769af70a33a8624ac496ac3e34d33366c0d",
|
||||
"sha256:e62bde7d5df3312acc528786ee801c472cae5078b1f1e42761c853ba7fe1072a",
|
||||
"sha256:e6ec696a268e8d730b42711537e500f7397afc06125c0e8fa9c8211386d315a5",
|
||||
"sha256:f176867f438ff2a43e6a837930153ca78fddb3ca94e378603a1e7b860d7869bf",
|
||||
"sha256:f8af980695b866255447703bf634551e67e1a4e1c2d2d26501858d9233d886d7",
|
||||
"sha256:f8e57f9c2367706a320b78e91f8bf9a3b03bf9069464eb7b54455fa340d03e4c",
|
||||
"sha256:f9d5d924970b07128c61c08eebee718686f4bd9838ef712a50468169520c953f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==3.7.0"
|
||||
"version": "==3.8.1"
|
||||
},
|
||||
"redis": {
|
||||
"extras": [
|
||||
@@ -1742,62 +1743,62 @@
|
||||
},
|
||||
"scikit-learn": {
|
||||
"hashes": [
|
||||
"sha256:0df87de9ce1c0140f2818beef310fb2e2afdc1e66fc9ad587965577f17733649",
|
||||
"sha256:14e4c88436ac96bf69eb6d746ac76a574c314a23c6961b7d344b38877f20fee1",
|
||||
"sha256:1754b0c2409d6ed5a3380512d0adcf182a01363c669033a2b55cca429ed86a81",
|
||||
"sha256:1afed6951bc9d2053c6ee9a518a466cbc9b07c6a3f9d43bfe734192b6125d508",
|
||||
"sha256:1d491ef66e37f4e812db7e6c8286520c2c3fc61b34bf5e59b67b4ce528de93af",
|
||||
"sha256:234b6bda70fdcae9e4abbbe028582ce99c280458665a155eed0b820599377d25",
|
||||
"sha256:2a3ee19211ded1a52ee37b0a7b373a8bfc66f95353af058a210b692bd4cda0dd",
|
||||
"sha256:4310bff71aa98b45b46cd26fa641309deb73a5d1c0461d181587ad4f30ea3c36",
|
||||
"sha256:4ba516fcdc73d60e7f48cbb0bccb9acbdb21807de3651531208aac73c758e3ab",
|
||||
"sha256:6145dfd9605b0b50ae72cdf72b61a2acd87501369a763b0d73d004710ebb76b5",
|
||||
"sha256:629e09f772ad42f657ca60a1a52342eef786218dd20cf1369a3b8d085e55ef8f",
|
||||
"sha256:712c1c69c45b58ef21635360b3d0a680ff7d83ac95b6f9b82cf9294070cda710",
|
||||
"sha256:78cd27b4669513b50db4f683ef41ea35b5dddc797bd2bbd990d49897fd1c8a46",
|
||||
"sha256:93d3d496ff1965470f9977d05e5ec3376fb1e63b10e4fda5e39d23c2d8969a30",
|
||||
"sha256:9f43dd527dabff5521af2786a2f8de5ba381e182ec7292663508901cf6ceaf6e",
|
||||
"sha256:a1e289f33f613cefe6707dead50db31930530dc386b6ccff176c786335a7b01c",
|
||||
"sha256:aa0029b78ef59af22cfbd833e8ace8526e4df90212db7ceccbea582ebb5d6794",
|
||||
"sha256:c02e27d65b0c7dc32f2c5eb601aaf5530b7a02bfbe92438188624524878336f2",
|
||||
"sha256:c540aaf44729ab5cd4bd5e394f2b375e65ceaea9cdd8c195788e70433d91bbc5",
|
||||
"sha256:ce03506ccf5f96b7e9030fea7eb148999b254c44c10182ac55857bc9b5d4815f",
|
||||
"sha256:d7cd3a77c32879311f2aa93466d3c288c955ef71d191503cf0677c3340ae8ae0"
|
||||
"sha256:1d0b25d9c651fd050555aadd57431b53d4cf664e749069da77f3d52c5ad14b3b",
|
||||
"sha256:36f0ea5d0f693cb247a073d21a4123bdf4172e470e6d163c12b74cbb1536cf38",
|
||||
"sha256:426d258fddac674fdf33f3cb2d54d26f49406e2599dbf9a32b4d1696091d4256",
|
||||
"sha256:44c62f2b124848a28fd695db5bc4da019287abf390bfce602ddc8aa1ec186aae",
|
||||
"sha256:45dee87ac5309bb82e3ea633955030df9bbcb8d2cdb30383c6cd483691c546cc",
|
||||
"sha256:49d64ef6cb8c093d883e5a36c4766548d974898d378e395ba41a806d0e824db8",
|
||||
"sha256:5460a1a5b043ae5ae4596b3126a4ec33ccba1b51e7ca2c5d36dac2169f62ab1d",
|
||||
"sha256:5cd7b524115499b18b63f0c96f4224eb885564937a0b3477531b2b63ce331904",
|
||||
"sha256:671e2f0c3f2c15409dae4f282a3a619601fa824d2c820e5b608d9d775f91780c",
|
||||
"sha256:68b8404841f944a4a1459b07198fa2edd41a82f189b44f3e1d55c104dbc2e40c",
|
||||
"sha256:81bf5d8bbe87643103334032dd82f7419bc8c8d02a763643a6b9a5c7288c5054",
|
||||
"sha256:8539a41b3d6d1af82eb629f9c57f37428ff1481c1e34dddb3b9d7af8ede67ac5",
|
||||
"sha256:87440e2e188c87db80ea4023440923dccbd56fbc2d557b18ced00fef79da0727",
|
||||
"sha256:90378e1747949f90c8f385898fff35d73193dfcaec3dd75d6b542f90c4e89755",
|
||||
"sha256:b0203c368058ab92efc6168a1507d388d41469c873e96ec220ca8e74079bf62e",
|
||||
"sha256:c97a50b05c194be9146d61fe87dbf8eac62b203d9e87a3ccc6ae9aed2dfaf361",
|
||||
"sha256:d36d0bc983336bbc1be22f9b686b50c964f593c8a9a913a792442af9bf4f5e68",
|
||||
"sha256:d762070980c17ba3e9a4a1e043ba0518ce4c55152032f1af0ca6f39b376b5928",
|
||||
"sha256:d9993d5e78a8148b1d0fdf5b15ed92452af5581734129998c26f481c46586d68",
|
||||
"sha256:daa1c471d95bad080c6e44b4946c9390a4842adc3082572c20e4f8884e39e959",
|
||||
"sha256:ff4effe5a1d4e8fed260a83a163f7dbf4f6087b54528d8880bab1d1377bd78be"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==1.4.1.post1"
|
||||
"version": "==1.4.2"
|
||||
},
|
||||
"scipy": {
|
||||
"hashes": [
|
||||
"sha256:196ebad3a4882081f62a5bf4aeb7326aa34b110e533aab23e4374fcccb0890dc",
|
||||
"sha256:408c68423f9de16cb9e602528be4ce0d6312b05001f3de61fe9ec8b1263cad08",
|
||||
"sha256:4bf5abab8a36d20193c698b0f1fc282c1d083c94723902c447e5d2f1780936a3",
|
||||
"sha256:4c1020cad92772bf44b8e4cdabc1df5d87376cb219742549ef69fc9fd86282dd",
|
||||
"sha256:5adfad5dbf0163397beb4aca679187d24aec085343755fcdbdeb32b3679f254c",
|
||||
"sha256:5e32847e08da8d895ce09d108a494d9eb78974cf6de23063f93306a3e419960c",
|
||||
"sha256:6546dc2c11a9df6926afcbdd8a3edec28566e4e785b915e849348c6dd9f3f490",
|
||||
"sha256:730badef9b827b368f351eacae2e82da414e13cf8bd5051b4bdfd720271a5371",
|
||||
"sha256:75ea2a144096b5e39402e2ff53a36fecfd3b960d786b7efd3c180e29c39e53f2",
|
||||
"sha256:78e4402e140879387187f7f25d91cc592b3501a2e51dfb320f48dfb73565f10b",
|
||||
"sha256:8b8066bce124ee5531d12a74b617d9ac0ea59245246410e19bca549656d9a40a",
|
||||
"sha256:8bee4993817e204d761dba10dbab0774ba5a8612e57e81319ea04d84945375ba",
|
||||
"sha256:913d6e7956c3a671de3b05ccb66b11bc293f56bfdef040583a7221d9e22a2e35",
|
||||
"sha256:95e5c750d55cf518c398a8240571b0e0782c2d5a703250872f36eaf737751338",
|
||||
"sha256:9c39f92041f490422924dfdb782527a4abddf4707616e07b021de33467f917bc",
|
||||
"sha256:a24024d45ce9a675c1fb8494e8e5244efea1c7a09c60beb1eeb80373d0fecc70",
|
||||
"sha256:a7ebda398f86e56178c2fa94cad15bf457a218a54a35c2a7b4490b9f9cb2676c",
|
||||
"sha256:b360f1b6b2f742781299514e99ff560d1fe9bd1bff2712894b52abe528d1fd1e",
|
||||
"sha256:bba1b0c7256ad75401c73e4b3cf09d1f176e9bd4248f0d3112170fb2ec4db067",
|
||||
"sha256:c3003652496f6e7c387b1cf63f4bb720951cfa18907e998ea551e6de51a04467",
|
||||
"sha256:e53958531a7c695ff66c2e7bb7b79560ffdc562e2051644c5576c39ff8efb563",
|
||||
"sha256:e646d8571804a304e1da01040d21577685ce8e2db08ac58e543eaca063453e1c",
|
||||
"sha256:e7e76cc48638228212c747ada851ef355c2bb5e7f939e10952bc504c11f4e372",
|
||||
"sha256:f5f00ebaf8de24d14b8449981a2842d404152774c1a1d880c901bf454cb8e2a1",
|
||||
"sha256:f7ce148dffcd64ade37b2df9315541f9adad6efcaa86866ee7dd5db0c8f041c3"
|
||||
"sha256:05f1432ba070e90d42d7fd836462c50bf98bd08bed0aa616c359eed8a04e3922",
|
||||
"sha256:09c74543c4fbeb67af6ce457f6a6a28e5d3739a87f62412e4a16e46f164f0ae5",
|
||||
"sha256:0fbcf8abaf5aa2dc8d6400566c1a727aed338b5fe880cde64907596a89d576fa",
|
||||
"sha256:109d391d720fcebf2fbe008621952b08e52907cf4c8c7efc7376822151820820",
|
||||
"sha256:1d2f7bb14c178f8b13ebae93f67e42b0a6b0fc50eba1cd8021c9b6e08e8fb1cd",
|
||||
"sha256:1e7626dfd91cdea5714f343ce1176b6c4745155d234f1033584154f60ef1ff42",
|
||||
"sha256:22789b56a999265431c417d462e5b7f2b487e831ca7bef5edeb56efe4c93f86e",
|
||||
"sha256:28e286bf9ac422d6beb559bc61312c348ca9b0f0dae0d7c5afde7f722d6ea13d",
|
||||
"sha256:33fde20efc380bd23a78a4d26d59fc8704e9b5fd9b08841693eb46716ba13d86",
|
||||
"sha256:45c08bec71d3546d606989ba6e7daa6f0992918171e2a6f7fbedfa7361c2de1e",
|
||||
"sha256:4dca18c3ffee287ddd3bc8f1dabaf45f5305c5afc9f8ab9cbfab855e70b2df5c",
|
||||
"sha256:5407708195cb38d70fd2d6bb04b1b9dd5c92297d86e9f9daae1576bd9e06f602",
|
||||
"sha256:58569af537ea29d3f78e5abd18398459f195546bb3be23d16677fb26616cc11e",
|
||||
"sha256:5e4a756355522eb60fcd61f8372ac2549073c8788f6114449b37e9e8104f15a5",
|
||||
"sha256:6bf9fe63e7a4bf01d3645b13ff2aa6dea023d38993f42aaac81a18b1bda7a82a",
|
||||
"sha256:8930ae3ea371d6b91c203b1032b9600d69c568e537b7988a3073dfe4d4774f21",
|
||||
"sha256:9ff7dad5d24a8045d836671e082a490848e8639cabb3dbdacb29f943a678683d",
|
||||
"sha256:a2f471de4d01200718b2b8927f7d76b5d9bde18047ea0fa8bd15c5ba3f26a1d6",
|
||||
"sha256:ac38c4c92951ac0f729c4c48c9e13eb3675d9986cc0c83943784d7390d540c78",
|
||||
"sha256:b2a3ff461ec4756b7e8e42e1c681077349a038f0686132d623fa404c0bee2551",
|
||||
"sha256:b5acd8e1dbd8dbe38d0004b1497019b2dbbc3d70691e65d69615f8a7292865d7",
|
||||
"sha256:b8434f6f3fa49f631fae84afee424e2483289dfc30a47755b4b4e6b07b2633a4",
|
||||
"sha256:ba419578ab343a4e0a77c0ef82f088238a93eef141b2b8017e46149776dfad4d",
|
||||
"sha256:d0de696f589681c2802f9090fff730c218f7c51ff49bf252b6a97ec4a5d19e8b",
|
||||
"sha256:dcbb9ea49b0167de4167c40eeee6e167caeef11effb0670b554d10b1e693a8b9"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==1.12.0"
|
||||
"version": "==1.13.0"
|
||||
},
|
||||
"setproctitle": {
|
||||
"hashes": [
|
||||
@@ -1912,11 +1913,11 @@
|
||||
},
|
||||
"sqlparse": {
|
||||
"hashes": [
|
||||
"sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3",
|
||||
"sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"
|
||||
"sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93",
|
||||
"sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==0.4.4"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==0.5.0"
|
||||
},
|
||||
"threadpoolctl": {
|
||||
"hashes": [
|
||||
@@ -1963,11 +1964,11 @@
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475",
|
||||
"sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"
|
||||
"sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0",
|
||||
"sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"
|
||||
],
|
||||
"markers": "python_version < '3.11'",
|
||||
"version": "==4.10.0"
|
||||
"version": "==4.11.0"
|
||||
},
|
||||
"tzdata": {
|
||||
"hashes": [
|
||||
@@ -2483,32 +2484,32 @@
|
||||
},
|
||||
"black": {
|
||||
"hashes": [
|
||||
"sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f",
|
||||
"sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93",
|
||||
"sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11",
|
||||
"sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0",
|
||||
"sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9",
|
||||
"sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5",
|
||||
"sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213",
|
||||
"sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d",
|
||||
"sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7",
|
||||
"sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837",
|
||||
"sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f",
|
||||
"sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395",
|
||||
"sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995",
|
||||
"sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f",
|
||||
"sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597",
|
||||
"sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959",
|
||||
"sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5",
|
||||
"sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb",
|
||||
"sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4",
|
||||
"sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7",
|
||||
"sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd",
|
||||
"sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"
|
||||
"sha256:1bb9ca06e556a09f7f7177bc7cb604e5ed2d2df1e9119e4f7d2f1f7071c32e5d",
|
||||
"sha256:21f9407063ec71c5580b8ad975653c66508d6a9f57bd008bb8691d273705adcd",
|
||||
"sha256:4396ca365a4310beef84d446ca5016f671b10f07abdba3e4e4304218d2c71d33",
|
||||
"sha256:44d99dfdf37a2a00a6f7a8dcbd19edf361d056ee51093b2445de7ca09adac965",
|
||||
"sha256:5cd5b4f76056cecce3e69b0d4c228326d2595f506797f40b9233424e2524c070",
|
||||
"sha256:64578cf99b6b46a6301bc28bdb89f9d6f9b592b1c5837818a177c98525dbe397",
|
||||
"sha256:64e60a7edd71fd542a10a9643bf369bfd2644de95ec71e86790b063aa02ff745",
|
||||
"sha256:652e55bb722ca026299eb74e53880ee2315b181dfdd44dca98e43448620ddec1",
|
||||
"sha256:6644f97a7ef6f401a150cca551a1ff97e03c25d8519ee0bbc9b0058772882665",
|
||||
"sha256:6ad001a9ddd9b8dfd1b434d566be39b1cd502802c8d38bbb1ba612afda2ef436",
|
||||
"sha256:71d998b73c957444fb7c52096c3843875f4b6b47a54972598741fe9a7f737fcb",
|
||||
"sha256:74eb9b5420e26b42c00a3ff470dc0cd144b80a766128b1771d07643165e08d0e",
|
||||
"sha256:75a2d0b4f5eb81f7eebc31f788f9830a6ce10a68c91fbe0fade34fff7a2836e6",
|
||||
"sha256:7852b05d02b5b9a8c893ab95863ef8986e4dda29af80bbbda94d7aee1abf8702",
|
||||
"sha256:7f2966b9b2b3b7104fca9d75b2ee856fe3fdd7ed9e47c753a4bb1a675f2caab8",
|
||||
"sha256:8e5537f456a22cf5cfcb2707803431d2feeb82ab3748ade280d6ccd0b40ed2e8",
|
||||
"sha256:d4e71cdebdc8efeb6deaf5f2deb28325f8614d48426bed118ecc2dcaefb9ebf3",
|
||||
"sha256:dae79397f367ac8d7adb6c779813328f6d690943f64b32983e896bcccd18cbad",
|
||||
"sha256:e3a3a092b8b756c643fe45f4624dbd5a389f770a4ac294cf4d0fce6af86addaf",
|
||||
"sha256:eb949f56a63c5e134dfdca12091e98ffb5fd446293ebae123d10fc1abad00b9e",
|
||||
"sha256:f07b69fda20578367eaebbd670ff8fc653ab181e1ff95d84497f9fa20e7d0641",
|
||||
"sha256:f95cece33329dc4aa3b0e1a771c41075812e46cf3d6e3f1dfe3d91ff09826ed2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==24.3.0"
|
||||
"version": "==24.4.0"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
@@ -2805,12 +2806,12 @@
|
||||
},
|
||||
"daphne": {
|
||||
"hashes": [
|
||||
"sha256:7228cd6a3ca5a9b11c9a1c1c0414dab1bfb4ddc55ff234b545db8d71f6c24938",
|
||||
"sha256:882fab39d0b90c6b2709b38116c95f660b6cf236600115dd7c13161fb98b3448"
|
||||
"sha256:618d1322bb4d875342b99dd2a10da2d9aae7ee3645f765965fdc1e658ea5290a",
|
||||
"sha256:fcbcace38eb86624ae247c7ffdc8ac12f155d7d19eafac4247381896d6f33761"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==4.1.0"
|
||||
"version": "==4.1.2"
|
||||
},
|
||||
"distlib": {
|
||||
"hashes": [
|
||||
@@ -2912,11 +2913,11 @@
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca",
|
||||
"sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"
|
||||
"sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc",
|
||||
"sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==3.6"
|
||||
"version": "==3.7"
|
||||
},
|
||||
"imagehash": {
|
||||
"hashes": [
|
||||
@@ -3057,12 +3058,12 @@
|
||||
},
|
||||
"mkdocs-material": {
|
||||
"hashes": [
|
||||
"sha256:06ae1275a72db1989cf6209de9e9ecdfbcfdbc24c58353877b2bb927dbe413e4",
|
||||
"sha256:14a2a60119a785e70e765dd033e6211367aca9fc70230e577c1cf6a326949571"
|
||||
"sha256:1e0e27fc9fe239f9064318acf548771a4629d5fd5dfd45444fd80a953fe21eb4",
|
||||
"sha256:a43f470947053fa2405c33995f282d24992c752a50114f23f30da9d8d0c57e62"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==9.5.17"
|
||||
"version": "==9.5.18"
|
||||
},
|
||||
"mkdocs-material-extensions": {
|
||||
"hashes": [
|
||||
@@ -3253,26 +3254,27 @@
|
||||
},
|
||||
"pyasn1": {
|
||||
"hashes": [
|
||||
"sha256:4439847c58d40b1d0a573d07e3856e95333f1976294494c325775aeca506eb58",
|
||||
"sha256:6d391a96e59b23130a5cfa74d6fd7f388dbbe26cc8f1edf39fdddf08d9d6676c"
|
||||
"sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c",
|
||||
"sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||
"version": "==0.5.1"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==0.6.0"
|
||||
},
|
||||
"pyasn1-modules": {
|
||||
"hashes": [
|
||||
"sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c",
|
||||
"sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d"
|
||||
"sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6",
|
||||
"sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||
"version": "==0.3.0"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==0.4.0"
|
||||
},
|
||||
"pycparser": {
|
||||
"hashes": [
|
||||
"sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9",
|
||||
"sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"
|
||||
"sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6",
|
||||
"sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"
|
||||
],
|
||||
"version": "==2.21"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.22"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
@@ -3284,11 +3286,11 @@
|
||||
},
|
||||
"pymdown-extensions": {
|
||||
"hashes": [
|
||||
"sha256:c70e146bdd83c744ffc766b4671999796aba18842b268510a329f7f64700d584",
|
||||
"sha256:f5cc7000d7ff0d1ce9395d216017fa4df3dde800afb1fb72d1c7d3fd35e710f4"
|
||||
"sha256:3539003ff0d5e219ba979d2dc961d18fcad5ac259e66c764482e8347b4c0503c",
|
||||
"sha256:91ca336caf414e1e5e0626feca86e145de9f85a3921a7bcbd32890b51738c428"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==10.7.1"
|
||||
"version": "==10.8"
|
||||
},
|
||||
"pyopenssl": {
|
||||
"hashes": [
|
||||
@@ -3474,102 +3476,102 @@
|
||||
},
|
||||
"regex": {
|
||||
"hashes": [
|
||||
"sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5",
|
||||
"sha256:086dd15e9435b393ae06f96ab69ab2d333f5d65cbe65ca5a3ef0ec9564dfe770",
|
||||
"sha256:094ba386bb5c01e54e14434d4caabf6583334090865b23ef58e0424a6286d3dc",
|
||||
"sha256:09da66917262d9481c719599116c7dc0c321ffcec4b1f510c4f8a066f8768105",
|
||||
"sha256:0ecf44ddf9171cd7566ef1768047f6e66975788258b1c6c6ca78098b95cf9a3d",
|
||||
"sha256:0fda75704357805eb953a3ee15a2b240694a9a514548cd49b3c5124b4e2ad01b",
|
||||
"sha256:11a963f8e25ab5c61348d090bf1b07f1953929c13bd2309a0662e9ff680763c9",
|
||||
"sha256:150c39f5b964e4d7dba46a7962a088fbc91f06e606f023ce57bb347a3b2d4630",
|
||||
"sha256:1b9d811f72210fa9306aeb88385b8f8bcef0dfbf3873410413c00aa94c56c2b6",
|
||||
"sha256:1e0eabac536b4cc7f57a5f3d095bfa557860ab912f25965e08fe1545e2ed8b4c",
|
||||
"sha256:22a86d9fff2009302c440b9d799ef2fe322416d2d58fc124b926aa89365ec482",
|
||||
"sha256:22f3470f7524b6da61e2020672df2f3063676aff444db1daa283c2ea4ed259d6",
|
||||
"sha256:263ef5cc10979837f243950637fffb06e8daed7f1ac1e39d5910fd29929e489a",
|
||||
"sha256:283fc8eed679758de38fe493b7d7d84a198b558942b03f017b1f94dda8efae80",
|
||||
"sha256:29171aa128da69afdf4bde412d5bedc335f2ca8fcfe4489038577d05f16181e5",
|
||||
"sha256:298dc6354d414bc921581be85695d18912bea163a8b23cac9a2562bbcd5088b1",
|
||||
"sha256:2aae8101919e8aa05ecfe6322b278f41ce2994c4a430303c4cd163fef746e04f",
|
||||
"sha256:2f4e475a80ecbd15896a976aa0b386c5525d0ed34d5c600b6d3ebac0a67c7ddf",
|
||||
"sha256:34e4af5b27232f68042aa40a91c3b9bb4da0eeb31b7632e0091afc4310afe6cb",
|
||||
"sha256:37f8e93a81fc5e5bd8db7e10e62dc64261bcd88f8d7e6640aaebe9bc180d9ce2",
|
||||
"sha256:3a17d3ede18f9cedcbe23d2daa8a2cd6f59fe2bf082c567e43083bba3fb00347",
|
||||
"sha256:3b1de218d5375cd6ac4b5493e0b9f3df2be331e86520f23382f216c137913d20",
|
||||
"sha256:43f7cd5754d02a56ae4ebb91b33461dc67be8e3e0153f593c509e21d219c5060",
|
||||
"sha256:4558410b7a5607a645e9804a3e9dd509af12fb72b9825b13791a37cd417d73a5",
|
||||
"sha256:4719bb05094d7d8563a450cf8738d2e1061420f79cfcc1fa7f0a44744c4d8f73",
|
||||
"sha256:4bfc2b16e3ba8850e0e262467275dd4d62f0d045e0e9eda2bc65078c0110a11f",
|
||||
"sha256:518440c991f514331f4850a63560321f833979d145d7d81186dbe2f19e27ae3d",
|
||||
"sha256:51f4b32f793812714fd5307222a7f77e739b9bc566dc94a18126aba3b92b98a3",
|
||||
"sha256:531ac6cf22b53e0696f8e1d56ce2396311254eb806111ddd3922c9d937151dae",
|
||||
"sha256:5cd05d0f57846d8ba4b71d9c00f6f37d6b97d5e5ef8b3c3840426a475c8f70f4",
|
||||
"sha256:5dd58946bce44b53b06d94aa95560d0b243eb2fe64227cba50017a8d8b3cd3e2",
|
||||
"sha256:60080bb3d8617d96f0fb7e19796384cc2467447ef1c491694850ebd3670bc457",
|
||||
"sha256:636ba0a77de609d6510235b7f0e77ec494d2657108f777e8765efc060094c98c",
|
||||
"sha256:67d3ccfc590e5e7197750fcb3a2915b416a53e2de847a728cfa60141054123d4",
|
||||
"sha256:68191f80a9bad283432385961d9efe09d783bcd36ed35a60fb1ff3f1ec2efe87",
|
||||
"sha256:7502534e55c7c36c0978c91ba6f61703faf7ce733715ca48f499d3dbbd7657e0",
|
||||
"sha256:7aa47c2e9ea33a4a2a05f40fcd3ea36d73853a2aae7b4feab6fc85f8bf2c9704",
|
||||
"sha256:7d2af3f6b8419661a0c421584cfe8aaec1c0e435ce7e47ee2a97e344b98f794f",
|
||||
"sha256:7e316026cc1095f2a3e8cc012822c99f413b702eaa2ca5408a513609488cb62f",
|
||||
"sha256:88ad44e220e22b63b0f8f81f007e8abbb92874d8ced66f32571ef8beb0643b2b",
|
||||
"sha256:88d1f7bef20c721359d8675f7d9f8e414ec5003d8f642fdfd8087777ff7f94b5",
|
||||
"sha256:89723d2112697feaa320c9d351e5f5e7b841e83f8b143dba8e2d2b5f04e10923",
|
||||
"sha256:8a0ccf52bb37d1a700375a6b395bff5dd15c50acb745f7db30415bae3c2b0715",
|
||||
"sha256:8c2c19dae8a3eb0ea45a8448356ed561be843b13cbc34b840922ddf565498c1c",
|
||||
"sha256:905466ad1702ed4acfd67a902af50b8db1feeb9781436372261808df7a2a7bca",
|
||||
"sha256:9852b76ab558e45b20bf1893b59af64a28bd3820b0c2efc80e0a70a4a3ea51c1",
|
||||
"sha256:98a2636994f943b871786c9e82bfe7883ecdaba2ef5df54e1450fa9869d1f756",
|
||||
"sha256:9aa1a67bbf0f957bbe096375887b2505f5d8ae16bf04488e8b0f334c36e31360",
|
||||
"sha256:9eda5f7a50141291beda3edd00abc2d4a5b16c29c92daf8d5bd76934150f3edc",
|
||||
"sha256:a6d1047952c0b8104a1d371f88f4ab62e6275567d4458c1e26e9627ad489b445",
|
||||
"sha256:a9b6d73353f777630626f403b0652055ebfe8ff142a44ec2cf18ae470395766e",
|
||||
"sha256:a9cc99d6946d750eb75827cb53c4371b8b0fe89c733a94b1573c9dd16ea6c9e4",
|
||||
"sha256:ad83e7545b4ab69216cef4cc47e344d19622e28aabec61574b20257c65466d6a",
|
||||
"sha256:b014333bd0217ad3d54c143de9d4b9a3ca1c5a29a6d0d554952ea071cff0f1f8",
|
||||
"sha256:b43523d7bc2abd757119dbfb38af91b5735eea45537ec6ec3a5ec3f9562a1c53",
|
||||
"sha256:b521dcecebc5b978b447f0f69b5b7f3840eac454862270406a39837ffae4e697",
|
||||
"sha256:b77e27b79448e34c2c51c09836033056a0547aa360c45eeeb67803da7b0eedaf",
|
||||
"sha256:b7a635871143661feccce3979e1727c4e094f2bdfd3ec4b90dfd4f16f571a87a",
|
||||
"sha256:b7fca9205b59c1a3d5031f7e64ed627a1074730a51c2a80e97653e3e9fa0d415",
|
||||
"sha256:ba1b30765a55acf15dce3f364e4928b80858fa8f979ad41f862358939bdd1f2f",
|
||||
"sha256:ba99d8077424501b9616b43a2d208095746fb1284fc5ba490139651f971d39d9",
|
||||
"sha256:c25a8ad70e716f96e13a637802813f65d8a6760ef48672aa3502f4c24ea8b400",
|
||||
"sha256:c3c4a78615b7762740531c27cf46e2f388d8d727d0c0c739e72048beb26c8a9d",
|
||||
"sha256:c40281f7d70baf6e0db0c2f7472b31609f5bc2748fe7275ea65a0b4601d9b392",
|
||||
"sha256:c7ad32824b7f02bb3c9f80306d405a1d9b7bb89362d68b3c5a9be53836caebdb",
|
||||
"sha256:cb3fe77aec8f1995611f966d0c656fdce398317f850d0e6e7aebdfe61f40e1cd",
|
||||
"sha256:cc038b2d8b1470364b1888a98fd22d616fba2b6309c5b5f181ad4483e0017861",
|
||||
"sha256:cc37b9aeebab425f11f27e5e9e6cf580be7206c6582a64467a14dda211abc232",
|
||||
"sha256:cc6bb9aa69aacf0f6032c307da718f61a40cf970849e471254e0e91c56ffca95",
|
||||
"sha256:d126361607b33c4eb7b36debc173bf25d7805847346dd4d99b5499e1fef52bc7",
|
||||
"sha256:d15b274f9e15b1a0b7a45d2ac86d1f634d983ca40d6b886721626c47a400bf39",
|
||||
"sha256:d166eafc19f4718df38887b2bbe1467a4f74a9830e8605089ea7a30dd4da8887",
|
||||
"sha256:d498eea3f581fbe1b34b59c697512a8baef88212f92e4c7830fcc1499f5b45a5",
|
||||
"sha256:d6f7e255e5fa94642a0724e35406e6cb7001c09d476ab5fce002f652b36d0c39",
|
||||
"sha256:d78bd484930c1da2b9679290a41cdb25cc127d783768a0369d6b449e72f88beb",
|
||||
"sha256:d865984b3f71f6d0af64d0d88f5733521698f6c16f445bb09ce746c92c97c586",
|
||||
"sha256:d902a43085a308cef32c0d3aea962524b725403fd9373dea18110904003bac97",
|
||||
"sha256:d94a1db462d5690ebf6ae86d11c5e420042b9898af5dcf278bd97d6bda065423",
|
||||
"sha256:da695d75ac97cb1cd725adac136d25ca687da4536154cdc2815f576e4da11c69",
|
||||
"sha256:db2a0b1857f18b11e3b0e54ddfefc96af46b0896fb678c85f63fb8c37518b3e7",
|
||||
"sha256:df26481f0c7a3f8739fecb3e81bc9da3fcfae34d6c094563b9d4670b047312e1",
|
||||
"sha256:e14b73607d6231f3cc4622809c196b540a6a44e903bcfad940779c80dffa7be7",
|
||||
"sha256:e2610e9406d3b0073636a3a2e80db05a02f0c3169b5632022b4e81c0364bcda5",
|
||||
"sha256:e692296c4cc2873967771345a876bcfc1c547e8dd695c6b89342488b0ea55cd8",
|
||||
"sha256:e693e233ac92ba83a87024e1d32b5f9ab15ca55ddd916d878146f4e3406b5c91",
|
||||
"sha256:e81469f7d01efed9b53740aedd26085f20d49da65f9c1f41e822a33992cb1590",
|
||||
"sha256:e8c7e08bb566de4faaf11984af13f6bcf6a08f327b13631d41d62592681d24fe",
|
||||
"sha256:ed19b3a05ae0c97dd8f75a5d8f21f7723a8c33bbc555da6bbe1f96c470139d3c",
|
||||
"sha256:efb2d82f33b2212898f1659fb1c2e9ac30493ac41e4d53123da374c3b5541e64",
|
||||
"sha256:f44dd4d68697559d007462b0a3a1d9acd61d97072b71f6d1968daef26bc744bd",
|
||||
"sha256:f72cbae7f6b01591f90814250e636065850c5926751af02bb48da94dfced7baa",
|
||||
"sha256:f7bc09bc9c29ebead055bcba136a67378f03d66bf359e87d0f7c759d6d4ffa31",
|
||||
"sha256:ff100b203092af77d1a5a7abe085b3506b7eaaf9abf65b73b7d6905b6cb76988"
|
||||
"sha256:00169caa125f35d1bca6045d65a662af0202704489fada95346cfa092ec23f39",
|
||||
"sha256:03576e3a423d19dda13e55598f0fd507b5d660d42c51b02df4e0d97824fdcae3",
|
||||
"sha256:03e68f44340528111067cecf12721c3df4811c67268b897fbe695c95f860ac42",
|
||||
"sha256:0534b034fba6101611968fae8e856c1698da97ce2efb5c2b895fc8b9e23a5834",
|
||||
"sha256:08dea89f859c3df48a440dbdcd7b7155bc675f2fa2ec8c521d02dc69e877db70",
|
||||
"sha256:0a38d151e2cdd66d16dab550c22f9521ba79761423b87c01dae0a6e9add79c0d",
|
||||
"sha256:0c8290b44d8b0af4e77048646c10c6e3aa583c1ca67f3b5ffb6e06cf0c6f0f89",
|
||||
"sha256:10188fe732dec829c7acca7422cdd1bf57d853c7199d5a9e96bb4d40db239c73",
|
||||
"sha256:1210365faba7c2150451eb78ec5687871c796b0f1fa701bfd2a4a25420482d26",
|
||||
"sha256:12f6a3f2f58bb7344751919a1876ee1b976fe08b9ffccb4bbea66f26af6017b9",
|
||||
"sha256:159dc4e59a159cb8e4e8f8961eb1fa5d58f93cb1acd1701d8aff38d45e1a84a6",
|
||||
"sha256:20b7a68444f536365af42a75ccecb7ab41a896a04acf58432db9e206f4e525d6",
|
||||
"sha256:23cff1b267038501b179ccbbd74a821ac4a7192a1852d1d558e562b507d46013",
|
||||
"sha256:2c72608e70f053643437bd2be0608f7f1c46d4022e4104d76826f0839199347a",
|
||||
"sha256:3399dd8a7495bbb2bacd59b84840eef9057826c664472e86c91d675d007137f5",
|
||||
"sha256:34422d5a69a60b7e9a07a690094e824b66f5ddc662a5fc600d65b7c174a05f04",
|
||||
"sha256:370c68dc5570b394cbaadff50e64d705f64debed30573e5c313c360689b6aadc",
|
||||
"sha256:3a1018e97aeb24e4f939afcd88211ace472ba566efc5bdf53fd8fd7f41fa7170",
|
||||
"sha256:3d5ac5234fb5053850d79dd8eb1015cb0d7d9ed951fa37aa9e6249a19aa4f336",
|
||||
"sha256:4313ab9bf6a81206c8ac28fdfcddc0435299dc88cad12cc6305fd0e78b81f9e4",
|
||||
"sha256:445ca8d3c5a01309633a0c9db57150312a181146315693273e35d936472df912",
|
||||
"sha256:479595a4fbe9ed8f8f72c59717e8cf222da2e4c07b6ae5b65411e6302af9708e",
|
||||
"sha256:4918fd5f8b43aa7ec031e0fef1ee02deb80b6afd49c85f0790be1dc4ce34cb50",
|
||||
"sha256:4aba818dcc7263852aabb172ec27b71d2abca02a593b95fa79351b2774eb1d2b",
|
||||
"sha256:4e819a806420bc010489f4e741b3036071aba209f2e0989d4750b08b12a9343f",
|
||||
"sha256:4facc913e10bdba42ec0aee76d029aedda628161a7ce4116b16680a0413f658a",
|
||||
"sha256:549c3584993772e25f02d0656ac48abdda73169fe347263948cf2b1cead622f3",
|
||||
"sha256:5c02fcd2bf45162280613d2e4a1ca3ac558ff921ae4e308ecb307650d3a6ee51",
|
||||
"sha256:5f580c651a72b75c39e311343fe6875d6f58cf51c471a97f15a938d9fe4e0d37",
|
||||
"sha256:62120ed0de69b3649cc68e2965376048793f466c5a6c4370fb27c16c1beac22d",
|
||||
"sha256:6295004b2dd37b0835ea5c14a33e00e8cfa3c4add4d587b77287825f3418d310",
|
||||
"sha256:65436dce9fdc0aeeb0a0effe0839cb3d6a05f45aa45a4d9f9c60989beca78b9c",
|
||||
"sha256:684008ec44ad275832a5a152f6e764bbe1914bea10968017b6feaecdad5736e0",
|
||||
"sha256:684e52023aec43bdf0250e843e1fdd6febbe831bd9d52da72333fa201aaa2335",
|
||||
"sha256:6cc38067209354e16c5609b66285af17a2863a47585bcf75285cab33d4c3b8df",
|
||||
"sha256:6f2f017c5be19984fbbf55f8af6caba25e62c71293213f044da3ada7091a4455",
|
||||
"sha256:743deffdf3b3481da32e8a96887e2aa945ec6685af1cfe2bcc292638c9ba2f48",
|
||||
"sha256:7571f19f4a3fd00af9341c7801d1ad1967fc9c3f5e62402683047e7166b9f2b4",
|
||||
"sha256:7731728b6568fc286d86745f27f07266de49603a6fdc4d19c87e8c247be452af",
|
||||
"sha256:785c071c982dce54d44ea0b79cd6dfafddeccdd98cfa5f7b86ef69b381b457d9",
|
||||
"sha256:78fddb22b9ef810b63ef341c9fcf6455232d97cfe03938cbc29e2672c436670e",
|
||||
"sha256:7bb966fdd9217e53abf824f437a5a2d643a38d4fd5fd0ca711b9da683d452969",
|
||||
"sha256:7cbc5d9e8a1781e7be17da67b92580d6ce4dcef5819c1b1b89f49d9678cc278c",
|
||||
"sha256:803b8905b52de78b173d3c1e83df0efb929621e7b7c5766c0843704d5332682f",
|
||||
"sha256:80b696e8972b81edf0af2a259e1b2a4a661f818fae22e5fa4fa1a995fb4a40fd",
|
||||
"sha256:81500ed5af2090b4a9157a59dbc89873a25c33db1bb9a8cf123837dcc9765047",
|
||||
"sha256:89ec7f2c08937421bbbb8b48c54096fa4f88347946d4747021ad85f1b3021b3c",
|
||||
"sha256:8ba6745440b9a27336443b0c285d705ce73adb9ec90e2f2004c64d95ab5a7598",
|
||||
"sha256:8c91e1763696c0eb66340c4df98623c2d4e77d0746b8f8f2bee2c6883fd1fe18",
|
||||
"sha256:8d015604ee6204e76569d2f44e5a210728fa917115bef0d102f4107e622b08d5",
|
||||
"sha256:8d1f86f3f4e2388aa3310b50694ac44daefbd1681def26b4519bd050a398dc5a",
|
||||
"sha256:8f83b6fd3dc3ba94d2b22717f9c8b8512354fd95221ac661784df2769ea9bba9",
|
||||
"sha256:8fc6976a3395fe4d1fbeb984adaa8ec652a1e12f36b56ec8c236e5117b585427",
|
||||
"sha256:904c883cf10a975b02ab3478bce652f0f5346a2c28d0a8521d97bb23c323cc8b",
|
||||
"sha256:911742856ce98d879acbea33fcc03c1d8dc1106234c5e7d068932c945db209c0",
|
||||
"sha256:91797b98f5e34b6a49f54be33f72e2fb658018ae532be2f79f7c63b4ae225145",
|
||||
"sha256:95399831a206211d6bc40224af1c635cb8790ddd5c7493e0bd03b85711076a53",
|
||||
"sha256:956b58d692f235cfbf5b4f3abd6d99bf102f161ccfe20d2fd0904f51c72c4c66",
|
||||
"sha256:98c1165f3809ce7774f05cb74e5408cd3aa93ee8573ae959a97a53db3ca3180d",
|
||||
"sha256:9ab40412f8cd6f615bfedea40c8bf0407d41bf83b96f6fc9ff34976d6b7037fd",
|
||||
"sha256:9df1bfef97db938469ef0a7354b2d591a2d438bc497b2c489471bec0e6baf7c4",
|
||||
"sha256:a01fe2305e6232ef3e8f40bfc0f0f3a04def9aab514910fa4203bafbc0bb4682",
|
||||
"sha256:a70b51f55fd954d1f194271695821dd62054d949efd6368d8be64edd37f55c86",
|
||||
"sha256:a7ccdd1c4a3472a7533b0a7aa9ee34c9a2bef859ba86deec07aff2ad7e0c3b94",
|
||||
"sha256:b340cccad138ecb363324aa26893963dcabb02bb25e440ebdf42e30963f1a4e0",
|
||||
"sha256:b74586dd0b039c62416034f811d7ee62810174bb70dffcca6439f5236249eb09",
|
||||
"sha256:b9d320b3bf82a39f248769fc7f188e00f93526cc0fe739cfa197868633d44701",
|
||||
"sha256:ba2336d6548dee3117520545cfe44dc28a250aa091f8281d28804aa8d707d93d",
|
||||
"sha256:ba8122e3bb94ecda29a8de4cf889f600171424ea586847aa92c334772d200331",
|
||||
"sha256:bd727ad276bb91928879f3aa6396c9a1d34e5e180dce40578421a691eeb77f47",
|
||||
"sha256:c21fc21a4c7480479d12fd8e679b699f744f76bb05f53a1d14182b31f55aac76",
|
||||
"sha256:c2d0e7cbb6341e830adcbfa2479fdeebbfbb328f11edd6b5675674e7a1e37730",
|
||||
"sha256:c2ef6f7990b6e8758fe48ad08f7e2f66c8f11dc66e24093304b87cae9037bb4a",
|
||||
"sha256:c4ed75ea6892a56896d78f11006161eea52c45a14994794bcfa1654430984b22",
|
||||
"sha256:cccc79a9be9b64c881f18305a7c715ba199e471a3973faeb7ba84172abb3f317",
|
||||
"sha256:d0800631e565c47520aaa04ae38b96abc5196fe8b4aa9bd864445bd2b5848a7a",
|
||||
"sha256:d2da13568eff02b30fd54fccd1e042a70fe920d816616fda4bf54ec705668d81",
|
||||
"sha256:d61ae114d2a2311f61d90c2ef1358518e8f05eafda76eaf9c772a077e0b465ec",
|
||||
"sha256:d83c2bc678453646f1a18f8db1e927a2d3f4935031b9ad8a76e56760461105dd",
|
||||
"sha256:dd5acc0a7d38fdc7a3a6fd3ad14c880819008ecb3379626e56b163165162cc46",
|
||||
"sha256:df79012ebf6f4efb8d307b1328226aef24ca446b3ff8d0e30202d7ebcb977a8c",
|
||||
"sha256:e0a2df336d1135a0b3a67f3bbf78a75f69562c1199ed9935372b82215cddd6e2",
|
||||
"sha256:e2f142b45c6fed48166faeb4303b4b58c9fcd827da63f4cf0a123c3480ae11fb",
|
||||
"sha256:e697e1c0238133589e00c244a8b676bc2cfc3ab4961318d902040d099fec7483",
|
||||
"sha256:e757d475953269fbf4b441207bb7dbdd1c43180711b6208e129b637792ac0b93",
|
||||
"sha256:e87ab229332ceb127a165612d839ab87795972102cb9830e5f12b8c9a5c1b508",
|
||||
"sha256:ea355eb43b11764cf799dda62c658c4d2fdb16af41f59bb1ccfec517b60bcb07",
|
||||
"sha256:ec7e0043b91115f427998febaa2beb82c82df708168b35ece3accb610b91fac1",
|
||||
"sha256:eeaa0b5328b785abc344acc6241cffde50dc394a0644a968add75fcefe15b9d4",
|
||||
"sha256:f2d80a6749724b37853ece57988b39c4e79d2b5fe2869a86e8aeae3bbeef9eb0",
|
||||
"sha256:fa454d26f2e87ad661c4f0c5a5fe4cf6aab1e307d1b94f16ffdfcb089ba685c0",
|
||||
"sha256:fb83cc090eac63c006871fd24db5e30a1f282faa46328572661c0a24a2323a08",
|
||||
"sha256:fd80d1280d473500d8086d104962a82d77bfbf2b118053824b7be28cd5a79ea5"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2023.12.25"
|
||||
"version": "==2024.4.16"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
@@ -3581,27 +3583,27 @@
|
||||
},
|
||||
"ruff": {
|
||||
"hashes": [
|
||||
"sha256:122de171a147c76ada00f76df533b54676f6e321e61bd8656ae54be326c10296",
|
||||
"sha256:3a05f3793ba25f194f395578579c546ca5d83e0195f992edc32e5907d142bfa3",
|
||||
"sha256:5e55771559c89272c3ebab23326dc23e7f813e492052391fe7950c1a5a139d89",
|
||||
"sha256:712e71283fc7d9f95047ed5f793bc019b0b0a29849b14664a60fd66c23b96da1",
|
||||
"sha256:87258e0d4b04046cf1d6cc1c56fadbf7a880cc3de1f7294938e923234cf9e498",
|
||||
"sha256:89b1e92b3bd9fca249153a97d23f29bed3992cff414b222fcd361d763fc53f12",
|
||||
"sha256:9d8605aa990045517c911726d21293ef4baa64f87265896e491a05461cae078d",
|
||||
"sha256:a067daaeb1dc2baf9b82a32dae67d154d95212080c80435eb052d95da647763d",
|
||||
"sha256:a532a90b4a18d3f722c124c513ffb5e5eaff0cc4f6d3aa4bda38e691b8600c9f",
|
||||
"sha256:a759d33a20c72f2dfa54dae6e85e1225b8e302e8ac655773aff22e542a300985",
|
||||
"sha256:a7b6e63194c68bca8e71f81de30cfa6f58ff70393cf45aab4c20f158227d5936",
|
||||
"sha256:aef5bd3b89e657007e1be6b16553c8813b221ff6d92c7526b7e0227450981eac",
|
||||
"sha256:d80a6b18a6c3b6ed25b71b05eba183f37d9bc8b16ace9e3d700997f00b74660b",
|
||||
"sha256:dabc62195bf54b8a7876add6e789caae0268f34582333cda340497c886111c39",
|
||||
"sha256:dc56bb16a63c1303bd47563c60482a1512721053d93231cf7e9e1c6954395a0e",
|
||||
"sha256:dfd3504e881082959b4160ab02f7a205f0fadc0a9619cc481982b6837b2fd4c0",
|
||||
"sha256:faeeae9905446b975dcf6d4499dc93439b131f1443ee264055c5716dd947af55"
|
||||
"sha256:0926cefb57fc5fced629603fbd1a23d458b25418681d96823992ba975f050c2b",
|
||||
"sha256:1c859f294f8633889e7d77de228b203eb0e9a03071b72b5989d89a0cf98ee262",
|
||||
"sha256:2c6e37f2e3cd74496a74af9a4fa67b547ab3ca137688c484749189bf3a686ceb",
|
||||
"sha256:2d9ef6231e3fbdc0b8c72404a1a0c46fd0dcea84efca83beb4681c318ea6a953",
|
||||
"sha256:6e68d248ed688b9d69fd4d18737edcbb79c98b251bba5a2b031ce2470224bdf9",
|
||||
"sha256:9485f54a7189e6f7433e0058cf8581bee45c31a25cd69009d2a040d1bd4bfaef",
|
||||
"sha256:a1eaf03d87e6a7cd5e661d36d8c6e874693cb9bc3049d110bc9a97b350680c43",
|
||||
"sha256:b34510141e393519a47f2d7b8216fec747ea1f2c81e85f076e9f2910588d4b64",
|
||||
"sha256:b90506f3d6d1f41f43f9b7b5ff845aeefabed6d2494307bc7b178360a8805252",
|
||||
"sha256:b92f03b4aa9fa23e1799b40f15f8b95cdc418782a567d6c43def65e1bbb7f1cf",
|
||||
"sha256:baa27d9d72a94574d250f42b7640b3bd2edc4c58ac8ac2778a8c82374bb27984",
|
||||
"sha256:c7d391e5936af5c9e252743d767c564670dc3889aff460d35c518ee76e4b26d7",
|
||||
"sha256:d2921ac03ce1383e360e8a95442ffb0d757a6a7ddd9a5be68561a671e0e5807e",
|
||||
"sha256:d592116cdbb65f8b1b7e2a2b48297eb865f6bdc20641879aa9d7b9c11d86db79",
|
||||
"sha256:eec8d185fe193ad053eda3a6be23069e0c8ba8c5d20bc5ace6e3b9e37d246d3f",
|
||||
"sha256:efd703a5975ac1998c2cc5e9494e13b28f31e66c616b0a76e206de2562e0843c",
|
||||
"sha256:f1ee41580bff1a651339eb3337c20c12f4037f6110a36ae4a2d864c52e5ef954"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.3.5"
|
||||
"version": "==0.4.1"
|
||||
},
|
||||
"scipy": {
|
||||
"hashes": [
|
||||
@@ -3643,11 +3645,11 @@
|
||||
},
|
||||
"setuptools": {
|
||||
"hashes": [
|
||||
"sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e",
|
||||
"sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"
|
||||
"sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987",
|
||||
"sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==69.2.0"
|
||||
"version": "==69.5.1"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
@@ -3702,11 +3704,11 @@
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475",
|
||||
"sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"
|
||||
"sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0",
|
||||
"sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"
|
||||
],
|
||||
"markers": "python_version < '3.11'",
|
||||
"version": "==4.10.0"
|
||||
"version": "==4.11.0"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
@@ -3769,45 +3771,45 @@
|
||||
},
|
||||
"zope-interface": {
|
||||
"hashes": [
|
||||
"sha256:02adbab560683c4eca3789cc0ac487dcc5f5a81cc48695ec247f00803cafe2fe",
|
||||
"sha256:14e02a6fc1772b458ebb6be1c276528b362041217b9ca37e52ecea2cbdce9fac",
|
||||
"sha256:25e0af9663eeac6b61b231b43c52293c2cb7f0c232d914bdcbfd3e3bd5c182ad",
|
||||
"sha256:2606955a06c6852a6cff4abeca38346ed01e83f11e960caa9a821b3626a4467b",
|
||||
"sha256:396f5c94654301819a7f3a702c5830f0ea7468d7b154d124ceac823e2419d000",
|
||||
"sha256:3b240883fb43160574f8f738e6d09ddbdbf8fa3e8cea051603d9edfd947d9328",
|
||||
"sha256:3b6c62813c63c543a06394a636978b22dffa8c5410affc9331ce6cdb5bfa8565",
|
||||
"sha256:4ae9793f114cee5c464cc0b821ae4d36e1eba961542c6086f391a61aee167b6f",
|
||||
"sha256:4bce517b85f5debe07b186fc7102b332676760f2e0c92b7185dd49c138734b70",
|
||||
"sha256:4d45d2ba8195850e3e829f1f0016066a122bfa362cc9dc212527fc3d51369037",
|
||||
"sha256:4dd374927c00764fcd6fe1046bea243ebdf403fba97a937493ae4be2c8912c2b",
|
||||
"sha256:506f5410b36e5ba494136d9fa04c548eaf1a0d9c442b0b0e7a0944db7620e0ab",
|
||||
"sha256:59f7374769b326a217d0b2366f1c176a45a4ff21e8f7cebb3b4a3537077eff85",
|
||||
"sha256:5ee9789a20b0081dc469f65ff6c5007e67a940d5541419ca03ef20c6213dd099",
|
||||
"sha256:6fc711acc4a1c702ca931fdbf7bf7c86f2a27d564c85c4964772dadf0e3c52f5",
|
||||
"sha256:75d2ec3d9b401df759b87bc9e19d1b24db73083147089b43ae748aefa63067ef",
|
||||
"sha256:76e0531d86523be7a46e15d379b0e975a9db84316617c0efe4af8338dc45b80c",
|
||||
"sha256:8af82afc5998e1f307d5e72712526dba07403c73a9e287d906a8aa2b1f2e33dd",
|
||||
"sha256:8f5d2c39f3283e461de3655e03faf10e4742bb87387113f787a7724f32db1e48",
|
||||
"sha256:97785604824981ec8c81850dd25c8071d5ce04717a34296eeac771231fbdd5cd",
|
||||
"sha256:a3046e8ab29b590d723821d0785598e0b2e32b636a0272a38409be43e3ae0550",
|
||||
"sha256:abb0b3f2cb606981c7432f690db23506b1db5899620ad274e29dbbbdd740e797",
|
||||
"sha256:ac7c2046d907e3b4e2605a130d162b1b783c170292a11216479bb1deb7cadebe",
|
||||
"sha256:af27b3fe5b6bf9cd01b8e1c5ddea0a0d0a1b8c37dc1c7452f1e90bf817539c6d",
|
||||
"sha256:b386b8b9d2b6a5e1e4eadd4e62335571244cb9193b7328c2b6e38b64cfda4f0e",
|
||||
"sha256:b66335bbdbb4c004c25ae01cc4a54fd199afbc1fd164233813c6d3c2293bb7e1",
|
||||
"sha256:d54f66c511ea01b9ef1d1a57420a93fbb9d48a08ec239f7d9c581092033156d0",
|
||||
"sha256:de125151a53ecdb39df3cb3deb9951ed834dd6a110a9e795d985b10bb6db4532",
|
||||
"sha256:de7916380abaef4bb4891740879b1afcba2045aee51799dfd6d6ca9bdc71f35f",
|
||||
"sha256:e2fefad268ff5c5b314794e27e359e48aeb9c8bb2cbb5748a071757a56f6bb8f",
|
||||
"sha256:e7b2bed4eea047a949296e618552d3fed00632dc1b795ee430289bdd0e3717f3",
|
||||
"sha256:e87698e2fea5ca2f0a99dff0a64ce8110ea857b640de536c76d92aaa2a91ff3a",
|
||||
"sha256:ede888382882f07b9e4cd942255921ffd9f2901684198b88e247c7eabd27a000",
|
||||
"sha256:f444de0565db46d26c9fa931ca14f497900a295bd5eba480fc3fad25af8c763e",
|
||||
"sha256:fa994e8937e8ccc7e87395b7b35092818905cf27c651e3ff3e7f29729f5ce3ce",
|
||||
"sha256:febceb04ee7dd2aef08c2ff3d6f8a07de3052fc90137c507b0ede3ea80c21440"
|
||||
"sha256:014bb94fe6bf1786da1aa044eadf65bc6437bcb81c451592987e5be91e70a91e",
|
||||
"sha256:01a0b3dd012f584afcf03ed814bce0fc40ed10e47396578621509ac031be98bf",
|
||||
"sha256:10cde8dc6b2fd6a1d0b5ca4be820063e46ddba417ab82bcf55afe2227337b130",
|
||||
"sha256:187f7900b63845dcdef1be320a523dbbdba94d89cae570edc2781eb55f8c2f86",
|
||||
"sha256:1b0c4c90e5eefca2c3e045d9f9ed9f1e2cdbe70eb906bff6b247e17119ad89a1",
|
||||
"sha256:22e8a218e8e2d87d4d9342aa973b7915297a08efbebea5b25900c73e78ed468e",
|
||||
"sha256:26c9a37fb395a703e39b11b00b9e921c48f82b6e32cc5851ad5d0618cd8876b5",
|
||||
"sha256:2bb78c12c1ad3a20c0d981a043d133299117b6854f2e14893b156979ed4e1d2c",
|
||||
"sha256:2c3cfb272bcb83650e6695d49ae0d14dd06dc694789a3d929f23758557a23d92",
|
||||
"sha256:2f32010ffb87759c6a3ad1c65ed4d2e38e51f6b430a1ca11cee901ec2b42e021",
|
||||
"sha256:3c8731596198198746f7ce2a4487a0edcbc9ea5e5918f0ab23c4859bce56055c",
|
||||
"sha256:40aa8c8e964d47d713b226c5baf5f13cdf3a3169c7a2653163b17ff2e2334d10",
|
||||
"sha256:4137025731e824eee8d263b20682b28a0bdc0508de9c11d6c6be54163e5b7c83",
|
||||
"sha256:46034be614d1f75f06e7dcfefba21d609b16b38c21fc912b01a99cb29e58febb",
|
||||
"sha256:483e118b1e075f1819b3c6ace082b9d7d3a6a5eb14b2b375f1b80a0868117920",
|
||||
"sha256:4d6b229f5e1a6375f206455cc0a63a8e502ed190fe7eb15e94a312dc69d40299",
|
||||
"sha256:567d54c06306f9c5b6826190628d66753b9f2b0422f4c02d7c6d2b97ebf0a24e",
|
||||
"sha256:5683aa8f2639016fd2b421df44301f10820e28a9b96382a6e438e5c6427253af",
|
||||
"sha256:600101f43a7582d5b9504a7c629a1185a849ce65e60fca0f6968dfc4b76b6d39",
|
||||
"sha256:62e32f02b3f26204d9c02c3539c802afc3eefb19d601a0987836ed126efb1f21",
|
||||
"sha256:69dedb790530c7ca5345899a1b4cb837cc53ba669051ea51e8c18f82f9389061",
|
||||
"sha256:72d5efecad16c619a97744a4f0b67ce1bcc88115aa82fcf1dc5be9bb403bcc0b",
|
||||
"sha256:8d407e0fd8015f6d5dfad481309638e1968d70e6644e0753f229154667dd6cd5",
|
||||
"sha256:a058e6cf8d68a5a19cb5449f42a404f0d6c2778b897e6ce8fadda9cea308b1b0",
|
||||
"sha256:a1adc14a2a9d5e95f76df625a9b39f4709267a483962a572e3f3001ef90ea6e6",
|
||||
"sha256:a56fe1261230093bfeedc1c1a6cd6f3ec568f9b07f031c9a09f46b201f793a85",
|
||||
"sha256:ad4524289d8dbd6fb5aa17aedb18f5643e7d48358f42c007a5ee51a2afc2a7c5",
|
||||
"sha256:afa0491a9f154cf8519a02026dc85a416192f4cb1efbbf32db4a173ba28b289a",
|
||||
"sha256:bf34840e102d1d0b2d39b1465918d90b312b1119552cebb61a242c42079817b9",
|
||||
"sha256:c40df4aea777be321b7e68facb901bc67317e94b65d9ab20fb96e0eb3c0b60a1",
|
||||
"sha256:d0e7321557c702bd92dac3c66a2f22b963155fdb4600133b6b29597f62b71b12",
|
||||
"sha256:d165d7774d558ea971cb867739fb334faf68fc4756a784e689e11efa3becd59e",
|
||||
"sha256:e78a183a3c2f555c2ad6aaa1ab572d1c435ba42f1dc3a7e8c82982306a19b785",
|
||||
"sha256:e8fa0fb05083a1a4216b4b881fdefa71c5d9a106e9b094cd4399af6b52873e91",
|
||||
"sha256:f83d6b4b22262d9a826c3bd4b2fbfafe1d0000f085ef8e44cd1328eea274ae6a",
|
||||
"sha256:f95bebd0afe86b2adc074df29edb6848fc4d474ff24075e2c263d698774e108d"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==6.2"
|
||||
"version": "==6.3"
|
||||
}
|
||||
},
|
||||
"typing-dev": {
|
||||
|
@@ -80,7 +80,7 @@ django_checks() {
|
||||
|
||||
search_index() {
|
||||
|
||||
local -r index_version=8
|
||||
local -r index_version=9
|
||||
local -r index_version_file=${DATA_DIR}/.index_version
|
||||
|
||||
if [[ (! -f "${index_version_file}") || $(<"${index_version_file}") != "$index_version" ]]; then
|
||||
|
@@ -140,6 +140,7 @@ document. Paperless only reports PDF metadata at this point.
|
||||
|
||||
- `/api/documents/<id>/notes/`: Retrieve notes for a document.
|
||||
- `/api/documents/<id>/share_links/`: Retrieve share links for a document.
|
||||
- `/api/documents/<id>/history/`: Retrieve history of changes for a document.
|
||||
|
||||
## Authorization
|
||||
|
||||
|
@@ -1,5 +1,25 @@
|
||||
# Changelog
|
||||
|
||||
## paperless-ngx 2.7.2
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: select dropdown background colors not visible in light mode [@shamoon](https://github.com/shamoon) ([#6323](https://github.com/paperless-ngx/paperless-ngx/pull/6323))
|
||||
- Fix: spacing in reset and incorrect display in saved views [@shamoon](https://github.com/shamoon) ([#6324](https://github.com/paperless-ngx/paperless-ngx/pull/6324))
|
||||
- Fix: disable invalid create endpoints [@shamoon](https://github.com/shamoon) ([#6320](https://github.com/paperless-ngx/paperless-ngx/pull/6320))
|
||||
- Fix: dont initialize page numbers, allow split with browser pdf viewer [@shamoon](https://github.com/shamoon) ([#6314](https://github.com/paperless-ngx/paperless-ngx/pull/6314))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>4 changes</summary>
|
||||
|
||||
- Fix: select dropdown background colors not visible in light mode [@shamoon](https://github.com/shamoon) ([#6323](https://github.com/paperless-ngx/paperless-ngx/pull/6323))
|
||||
- Fix: spacing in reset and incorrect display in saved views [@shamoon](https://github.com/shamoon) ([#6324](https://github.com/paperless-ngx/paperless-ngx/pull/6324))
|
||||
- Fix: disable invalid create endpoints [@shamoon](https://github.com/shamoon) ([#6320](https://github.com/paperless-ngx/paperless-ngx/pull/6320))
|
||||
- Fix: dont initialize page numbers, allow split with browser pdf viewer [@shamoon](https://github.com/shamoon) ([#6314](https://github.com/paperless-ngx/paperless-ngx/pull/6314))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.7.1
|
||||
|
||||
### Bug Fixes
|
||||
|
@@ -241,6 +241,11 @@ permissions can be granted to limit access to certain parts of the UI (and corre
|
||||
|
||||
Superusers can access all parts of the front and backend application as well as any and all objects.
|
||||
|
||||
#### Admin Status
|
||||
|
||||
Admin status (Django 'staff status') grants access to viewing the paperless logs and the system status dialog
|
||||
as well as accessing the Django backend.
|
||||
|
||||
#### Detailed Explanation of Global Permissions {#global-permissions}
|
||||
|
||||
Global permissions define what areas of the app and API endpoints the user can access. For example, they
|
||||
@@ -249,7 +254,6 @@ still have "object-level" permissions.
|
||||
|
||||
| Type | Details |
|
||||
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Admin | _View_ or higher permissions grants access to the logs view as well as the system status. |
|
||||
| AppConfig | _Change_ or higher permissions grants access to the "Application Configuration" area. |
|
||||
| Correspondent | Grants global permissions to add, edit, delete or view Correspondents. |
|
||||
| CustomField | Grants global permissions to add, edit, delete or view Custom Fields. |
|
||||
@@ -468,6 +472,12 @@ Paperless-ngx supports 3 basic editing operations for PDFs (these operations can
|
||||
|
||||
Note that rotation alters the Paperless-ngx _original_ file, which would, for example, invalidate a digital signature.
|
||||
|
||||
## Document History
|
||||
|
||||
As of version 2.7, Paperless-ngx automatically records all changes to a document and records this in an audit log. The feature requires [`PAPERLESS_AUDIT_LOG_ENABLED`](configuration.md#PAPERLESS_AUDIT_LOG_ENABLED) be enabled, which it is by default as of version 2.7.
|
||||
Changes to documents are visible under the "History" tab. Note that certain changes such as those made by workflows, record the 'actor'
|
||||
as "System".
|
||||
|
||||
## Best practices {#basic-searching}
|
||||
|
||||
Paperless offers a couple tools that help you organize your document
|
||||
|
@@ -71,7 +71,17 @@ if ! docker stats --no-stream &> /dev/null ; then
|
||||
sleep 3
|
||||
fi
|
||||
|
||||
default_time_zone=$(timedatectl show -p Timezone --value)
|
||||
# Added handling for timezone for busybox based linux, not having timedatectl available (i.e. QNAP QTS)
|
||||
# if neither timedatectl nor /etc/TZ is succeeding, defaulting to GMT.
|
||||
if command -v timedatectl &> /dev/null ; then
|
||||
default_time_zone=$(timedatectl show -p Timezone --value)
|
||||
elif [ -f /etc/TZ ] && [ -f /etc/tzlist ] ; then
|
||||
TZ=$(cat /etc/TZ)
|
||||
default_time_zone=$(grep -B 1 -m 1 "$TZ" /etc/tzlist | head -1 | cut -f 2 -d =)
|
||||
else
|
||||
echo "WARN: unable to detect timezone, defaulting to Etc/UTC"
|
||||
default_time_zone="Etc/UTC"
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
|
36
paperless-ngx.code-workspace
Normal file
36
paperless-ngx.code-workspace
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "./src",
|
||||
"name": "Backend"
|
||||
},
|
||||
{
|
||||
"path": "./src-ui",
|
||||
"name": "Frontend"
|
||||
},
|
||||
{
|
||||
"path": "./.github",
|
||||
"name": "CI/CD"
|
||||
},
|
||||
{
|
||||
"path": "./docs",
|
||||
"name": "Documentation"
|
||||
}
|
||||
|
||||
],
|
||||
"settings": {
|
||||
"files.exclude": {
|
||||
"**/__pycache__": true,
|
||||
"**/.mypy_cache": true,
|
||||
"**/.ruff_cache": true,
|
||||
"**/.pytest_cache": true,
|
||||
"**/.idea": true,
|
||||
"**/.venv": true,
|
||||
"**/.coverage": true,
|
||||
"**/coverage.json": true
|
||||
}
|
||||
}
|
||||
}
|
@@ -124,7 +124,7 @@
|
||||
"content": {
|
||||
"size": -1,
|
||||
"mimeType": "application/json",
|
||||
"text": "{\"count\":6,\"next\":null,\"previous\":null,\"all\":[8,17,7,4,11,15],\"results\":[{\"id\":8,\"name\":\"Correspondent 2\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":3,\"value\":\"2\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":17,\"name\":\"In the Last Month\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":20,\"value\":\"created:[-1 month to now]\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":7,\"name\":\"Inbox\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"9\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":4,\"name\":\"Recently Added\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[],\"owner\":\"2\",\"user_can_change\":true},{\"id\":11,\"name\":\"Tag: Another Sample Tag\",\"show_on_dashboard\":false,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"4\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":15,\"name\":\"View ASN not empty\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":18,\"value\":\"false\"}],\"owner\":\"2\",\"user_can_change\":true}]}"
|
||||
"text": "{\"count\":6,\"next\":null,\"previous\":null,\"all\":[8,17,7,4,11,15],\"results\":[{\"id\":8,\"name\":\"Correspondent 2\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":3,\"value\":\"2\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":17,\"name\":\"In the Last Month\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":20,\"value\":\"created:[-1 month to now]\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":7,\"name\":\"Inbox\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"9\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":4,\"name\":\"Recently Added\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":11,\"name\":\"Tag: Another Sample Tag\",\"show_on_dashboard\":false,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"4\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":15,\"name\":\"View ASN not empty\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":18,\"value\":\"false\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]}]}"
|
||||
},
|
||||
"headersSize": -1,
|
||||
"bodySize": -1,
|
||||
|
@@ -124,7 +124,7 @@
|
||||
"content": {
|
||||
"size": -1,
|
||||
"mimeType": "application/json",
|
||||
"text": "{\"count\":6,\"next\":null,\"previous\":null,\"all\":[8,17,7,4,11,15],\"results\":[{\"id\":8,\"name\":\"Correspondent 2\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":3,\"value\":\"2\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":17,\"name\":\"In the Last Month\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":20,\"value\":\"created:[-1 month to now]\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":7,\"name\":\"Inbox\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"9\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":4,\"name\":\"Recently Added\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[],\"owner\":\"2\",\"user_can_change\":true},{\"id\":11,\"name\":\"Tag: Another Sample Tag\",\"show_on_dashboard\":false,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"4\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":15,\"name\":\"View ASN not empty\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":18,\"value\":\"false\"}],\"owner\":\"2\",\"user_can_change\":true}]}"
|
||||
"text": "{\"count\":6,\"next\":null,\"previous\":null,\"all\":[8,17,7,4,11,15],\"results\":[{\"id\":8,\"name\":\"Correspondent 2\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":3,\"value\":\"2\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":17,\"name\":\"In the Last Month\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":20,\"value\":\"created:[-1 month to now]\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":7,\"name\":\"Inbox\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"9\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":4,\"name\":\"Recently Added\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":11,\"name\":\"Tag: Another Sample Tag\",\"show_on_dashboard\":false,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"4\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":15,\"name\":\"View ASN not empty\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":18,\"value\":\"false\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]}]}"
|
||||
},
|
||||
"headersSize": -1,
|
||||
"bodySize": -1,
|
||||
|
@@ -124,7 +124,7 @@
|
||||
"content": {
|
||||
"size": -1,
|
||||
"mimeType": "application/json",
|
||||
"text": "{\"count\":6,\"next\":null,\"previous\":null,\"all\":[8,17,7,4,11,15],\"results\":[{\"id\":8,\"name\":\"Correspondent 2\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":3,\"value\":\"2\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":17,\"name\":\"In the Last Month\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":20,\"value\":\"created:[-1 month to now]\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":7,\"name\":\"Inbox\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"9\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":4,\"name\":\"Recently Added\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[],\"owner\":\"2\",\"user_can_change\":true},{\"id\":11,\"name\":\"Tag: Another Sample Tag\",\"show_on_dashboard\":false,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"4\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":15,\"name\":\"View ASN not empty\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":18,\"value\":\"false\"}],\"owner\":\"2\",\"user_can_change\":true}]}"
|
||||
"text": "{\"count\":6,\"next\":null,\"previous\":null,\"all\":[8,17,7,4,11,15],\"results\":[{\"id\":8,\"name\":\"Correspondent 2\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":3,\"value\":\"2\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":17,\"name\":\"In the Last Month\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":20,\"value\":\"created:[-1 month to now]\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":7,\"name\":\"Inbox\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"9\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":4,\"name\":\"Recently Added\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":11,\"name\":\"Tag: Another Sample Tag\",\"show_on_dashboard\":false,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"4\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":15,\"name\":\"View ASN not empty\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":18,\"value\":\"false\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]}]}"
|
||||
},
|
||||
"headersSize": -1,
|
||||
"bodySize": -1,
|
||||
|
@@ -124,7 +124,7 @@
|
||||
"content": {
|
||||
"size": -1,
|
||||
"mimeType": "application/json",
|
||||
"text": "{\"count\":6,\"next\":null,\"previous\":null,\"all\":[8,17,7,4,11,15],\"results\":[{\"id\":8,\"name\":\"Correspondent 2\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":3,\"value\":\"2\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":7,\"name\":\"Inbox\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"9\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":11,\"name\":\"Tag: Another Sample Tag\",\"show_on_dashboard\":false,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"4\"}],\"owner\":\"2\",\"user_can_change\":true}]}"
|
||||
"text": "{\"count\":6,\"next\":null,\"previous\":null,\"all\":[8,17,7,4,11,15],\"results\":[{\"id\":8,\"name\":\"Correspondent 2\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":3,\"value\":\"2\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":17,\"name\":\"In the Last Month\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":20,\"value\":\"created:[-1 month to now]\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":7,\"name\":\"Inbox\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"9\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":4,\"name\":\"Recently Added\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":11,\"name\":\"Tag: Another Sample Tag\",\"show_on_dashboard\":false,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"4\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":15,\"name\":\"View ASN not empty\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":18,\"value\":\"false\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]}]}"
|
||||
},
|
||||
"headersSize": -1,
|
||||
"bodySize": -1,
|
||||
|
@@ -81,14 +81,15 @@ test('text filtering', async ({ page }) => {
|
||||
test('date filtering', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR3, { notFound: 'fallback' })
|
||||
await page.goto('/documents')
|
||||
await page.getByRole('button', { name: 'Created' }).click()
|
||||
await page.getByRole('menuitem', { name: 'Last 3 months' }).click()
|
||||
await page.getByRole('button', { name: 'Dates' }).click()
|
||||
await page.getByRole('menuitem', { name: 'Last 3 months' }).first().click()
|
||||
await expect(page.locator('pngx-document-list')).toHaveText(/one document/i)
|
||||
await page.getByRole('button', { name: 'Created Clear selected' }).click()
|
||||
await page.getByRole('button', { name: 'Created' }).click()
|
||||
await page.getByRole('button', { name: 'Dates Clear selected' }).click()
|
||||
await page.getByRole('button', { name: 'Dates' }).click()
|
||||
await page
|
||||
.getByRole('menuitem', { name: 'After mm/dd/yyyy' })
|
||||
.getByRole('button')
|
||||
.first()
|
||||
.click()
|
||||
await page.getByRole('combobox', { name: 'Select month' }).selectOption('12')
|
||||
await page.getByRole('combobox', { name: 'Select year' }).selectOption('2022')
|
||||
@@ -138,11 +139,11 @@ test('sorting', async ({ page }) => {
|
||||
test('change views', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR5, { notFound: 'fallback' })
|
||||
await page.goto('/documents')
|
||||
await page.locator('pngx-page-header label').first().click()
|
||||
await page.locator('.btn-group label').first().click()
|
||||
await expect(page.locator('pngx-document-list table')).toBeVisible()
|
||||
await page.locator('pngx-page-header label').nth(1).click()
|
||||
await page.locator('.btn-group label').nth(1).click()
|
||||
await expect(page.locator('pngx-document-card-small').first()).toBeAttached()
|
||||
await page.locator('pngx-page-header label').nth(2).click()
|
||||
await page.locator('.btn-group label').nth(2).click()
|
||||
await expect(page.locator('pngx-document-card-large').first()).toBeAttached()
|
||||
})
|
||||
|
||||
|
1334
src-ui/messages.xlf
1334
src-ui/messages.xlf
File diff suppressed because it is too large
Load Diff
6
src-ui/package-lock.json
generated
6
src-ui/package-lock.json
generated
@@ -17531,9 +17531,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
|
||||
"integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==",
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"chownr": "^2.0.0",
|
||||
|
@@ -141,10 +141,7 @@ export const routes: Routes = [
|
||||
component: LogsComponent,
|
||||
canActivate: [PermissionsGuard],
|
||||
data: {
|
||||
requiredPermission: {
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.Admin,
|
||||
},
|
||||
requireAdmin: true,
|
||||
},
|
||||
},
|
||||
// redirect old paths
|
||||
|
@@ -31,7 +31,7 @@ import { ToastsComponent } from './components/common/toasts/toasts.component'
|
||||
import { FilterEditorComponent } from './components/document-list/filter-editor/filter-editor.component'
|
||||
import { FilterableDropdownComponent } from './components/common/filterable-dropdown/filterable-dropdown.component'
|
||||
import { ToggleableDropdownButtonComponent } from './components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component'
|
||||
import { DateDropdownComponent } from './components/common/date-dropdown/date-dropdown.component'
|
||||
import { DatesDropdownComponent } from './components/common/dates-dropdown/dates-dropdown.component'
|
||||
import { DocumentCardLargeComponent } from './components/document-list/document-card-large/document-card-large.component'
|
||||
import { DocumentCardSmallComponent } from './components/document-list/document-card-small/document-card-small.component'
|
||||
import { BulkEditorComponent } from './components/document-list/bulk-editor/bulk-editor.component'
|
||||
@@ -119,6 +119,9 @@ import { NgxFilesizeModule } from 'ngx-filesize'
|
||||
import { RotateConfirmDialogComponent } from './components/common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
|
||||
import { MergeConfirmDialogComponent } from './components/common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component'
|
||||
import { SplitConfirmDialogComponent } from './components/common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component'
|
||||
import { DocumentHistoryComponent } from './components/document-history/document-history.component'
|
||||
import { DragDropSelectComponent } from './components/common/input/drag-drop-select/drag-drop-select.component'
|
||||
import { CustomFieldDisplayComponent } from './components/common/custom-field-display/custom-field-display.component'
|
||||
import {
|
||||
airplane,
|
||||
archive,
|
||||
@@ -137,7 +140,9 @@ import {
|
||||
boxes,
|
||||
calendar,
|
||||
calendarEvent,
|
||||
calendarEventFill,
|
||||
cardChecklist,
|
||||
cardHeading,
|
||||
caretDown,
|
||||
caretUp,
|
||||
chatLeftText,
|
||||
@@ -231,7 +236,9 @@ const icons = {
|
||||
boxes,
|
||||
calendar,
|
||||
calendarEvent,
|
||||
calendarEventFill,
|
||||
cardChecklist,
|
||||
cardHeading,
|
||||
caretDown,
|
||||
caretUp,
|
||||
chatLeftText,
|
||||
@@ -402,7 +409,7 @@ function initializeApp(settings: SettingsService) {
|
||||
FilterEditorComponent,
|
||||
FilterableDropdownComponent,
|
||||
ToggleableDropdownButtonComponent,
|
||||
DateDropdownComponent,
|
||||
DatesDropdownComponent,
|
||||
DocumentCardLargeComponent,
|
||||
DocumentCardSmallComponent,
|
||||
BulkEditorComponent,
|
||||
@@ -472,6 +479,9 @@ function initializeApp(settings: SettingsService) {
|
||||
RotateConfirmDialogComponent,
|
||||
MergeConfirmDialogComponent,
|
||||
SplitConfirmDialogComponent,
|
||||
DocumentHistoryComponent,
|
||||
DragDropSelectComponent,
|
||||
CustomFieldDisplayComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
@@ -7,29 +7,30 @@
|
||||
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()">
|
||||
<i-bs class="me-1" name="airplane"></i-bs> <ng-container i18n>Start tour</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary position-relative ms-md-5 me-1" (click)="showSystemStatus()"
|
||||
[disabled]="!systemStatus"
|
||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }">
|
||||
@if (!systemStatus) {
|
||||
<div class="spinner-border spinner-border-sm me-1 h-75" role="status"></div>
|
||||
} @else {
|
||||
<i-bs class="me-2" name="card-checklist"></i-bs>
|
||||
@if (systemStatusHasErrors) {
|
||||
<span class="badge bg-body position-absolute top-0 start-100 translate-middle rounded-pill p-0">
|
||||
<i-bs name="exclamation-circle-fill" class="text-danger" width="1.75em" height="1.75em"></i-bs>
|
||||
</span>
|
||||
@if (permissionsService.isAdmin()) {
|
||||
<button class="btn btn-sm btn-outline-primary position-relative ms-md-5 me-1" (click)="showSystemStatus()"
|
||||
[disabled]="!systemStatus">
|
||||
@if (!systemStatus) {
|
||||
<div class="spinner-border spinner-border-sm me-1 h-75" role="status"></div>
|
||||
} @else {
|
||||
<span class="badge bg-body position-absolute top-0 start-100 translate-middle rounded-pill p-0">
|
||||
<i-bs name="check-circle-fill" class="text-primary" width="1.75em" height="1.75em"></i-bs>
|
||||
</span>
|
||||
<i-bs class="me-2" name="card-checklist"></i-bs>
|
||||
@if (systemStatusHasErrors) {
|
||||
<span class="badge bg-body position-absolute top-0 start-100 translate-middle rounded-pill p-0">
|
||||
<i-bs name="exclamation-circle-fill" class="text-danger" width="1.75em" height="1.75em"></i-bs>
|
||||
</span>
|
||||
} @else {
|
||||
<span class="badge bg-body position-absolute top-0 start-100 translate-middle rounded-pill p-0">
|
||||
<i-bs name="check-circle-fill" class="text-primary" width="1.75em" height="1.75em"></i-bs>
|
||||
</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
<ng-container i18n>System Status</ng-container>
|
||||
</button>
|
||||
<a *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }" class="btn btn-sm btn-primary" href="admin/" target="_blank">
|
||||
<ng-container i18n>Open Django Admin</ng-container>
|
||||
<i-bs name="arrow-up-right"></i-bs>
|
||||
</a>
|
||||
<ng-container i18n>System Status</ng-container>
|
||||
</button>
|
||||
<a class="btn btn-sm btn-primary" href="admin/" target="_blank">
|
||||
<ng-container i18n>Open Django Admin</ng-container>
|
||||
<i-bs name="arrow-up-right"></i-bs>
|
||||
</a>
|
||||
}
|
||||
</pngx-page-header>
|
||||
|
||||
<form [formGroup]="settingsForm" (ngSubmit)="saveSettings()">
|
||||
@@ -319,52 +320,71 @@
|
||||
</div>
|
||||
|
||||
<h4 i18n>Views</h4>
|
||||
<div formGroupName="savedViews">
|
||||
<ul class="list-group" formGroupName="savedViews">
|
||||
|
||||
@for (view of savedViews; track view) {
|
||||
<li class="list-group-item py-3">
|
||||
<div [formGroupName]="view.id" class="row">
|
||||
<div class="mb-3 col">
|
||||
<label class="form-label" for="name_{{view.id}}" i18n>Name</label>
|
||||
<input type="text" class="form-control" formControlName="name" id="name_{{view.id}}">
|
||||
</div>
|
||||
<div class="mb-2 col">
|
||||
<label class="form-label" for="show_on_dashboard_{{view.id}}" i18n> <span class="visually-hidden">Appears on</span></label>
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" class="form-check-input" id="show_on_dashboard_{{view.id}}" formControlName="show_on_dashboard">
|
||||
<label class="form-check-label" for="show_on_dashboard_{{view.id}}" i18n>Show on dashboard</label>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<pngx-input-text title="Name" formControlName="name"></pngx-input-text>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" class="form-check-input" id="show_in_sidebar_{{view.id}}" formControlName="show_in_sidebar">
|
||||
<label class="form-check-label" for="show_in_sidebar_{{view.id}}" i18n>Show in sidebar</label>
|
||||
<div class="col">
|
||||
<div class="form-check form-switch mt-3">
|
||||
<input type="checkbox" class="form-check-input" id="show_on_dashboard_{{view.id}}" formControlName="show_on_dashboard">
|
||||
<label class="form-check-label" for="show_on_dashboard_{{view.id}}" i18n>Show on dashboard</label>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" class="form-check-input" id="show_in_sidebar_{{view.id}}" formControlName="show_in_sidebar">
|
||||
<label class="form-check-label" for="show_in_sidebar_{{view.id}}" i18n>Show in sidebar</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="form-label" for="name_{{view.id}}" i18n>Actions</label>
|
||||
<pngx-confirm-button
|
||||
label="Delete"
|
||||
i18n-label
|
||||
(confirm)="deleteSavedView(view)"
|
||||
*pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.SavedView }"
|
||||
buttonClasses="btn-sm btn-outline-danger form-control"
|
||||
iconName="trash">
|
||||
</pngx-confirm-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2 col-auto">
|
||||
<label class="form-label" for="name_{{view.id}}" i18n>Actions</label>
|
||||
|
||||
<pngx-confirm-button
|
||||
label="Delete"
|
||||
i18n-label
|
||||
(confirm)="deleteSavedView(view)"
|
||||
*pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.SavedView }"
|
||||
buttonClasses="btn-sm btn-outline-danger form-control"
|
||||
iconName="trash">
|
||||
</pngx-confirm-button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<pngx-input-number i18n-title title="Documents page size" [showAdd]="false" formControlName="page_size"></pngx-input-number>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label" for="display_mode_{{view.id}}" i18n>Display as</label>
|
||||
<select class="form-select" formControlName="display_mode">
|
||||
<option [ngValue]="DisplayMode.TABLE" i18n>Table</option>
|
||||
<option [ngValue]="DisplayMode.SMALL_CARDS" i18n>Small Cards</option>
|
||||
<option [ngValue]="DisplayMode.LARGE_CARDS" i18n>Large Cards</option>
|
||||
</select>
|
||||
</div>
|
||||
@if (displayFields) {
|
||||
<pngx-input-drag-drop-select i18n-title title="Show" i18n-emptyText emptyText="Default" [items]="displayFields" formControlName="display_fields"></pngx-input-drag-drop-select>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
|
||||
@if (savedViews && savedViews.length === 0) {
|
||||
<div i18n>No saved views defined.</div>
|
||||
<li class="list-group-item">
|
||||
<div i18n>No saved views defined.</div>
|
||||
</li>
|
||||
}
|
||||
|
||||
@if (!savedViews) {
|
||||
<div>
|
||||
<li class="list-group-item">
|
||||
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||
<div class="visually-hidden" i18n>Loading...</div>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
@@ -373,4 +393,5 @@
|
||||
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
|
||||
|
||||
<button type="submit" class="btn btn-primary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
|
||||
<button type="button" (click)="reset()" class="btn btn-secondary ms-2 mb-2" [disabled]="(isDirty$ | async) === false" i18n>Cancel</button>
|
||||
</form>
|
||||
|
@@ -48,6 +48,8 @@ import {
|
||||
InstallType,
|
||||
SystemStatusItemStatus,
|
||||
} from 'src/app/data/system-status'
|
||||
import { DragDropSelectComponent } from '../../common/input/drag-drop-select/drag-drop-select.component'
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop'
|
||||
|
||||
const savedViews = [
|
||||
{ id: 1, name: 'view1', show_in_sidebar: true, show_on_dashboard: true },
|
||||
@@ -96,6 +98,7 @@ describe('SettingsComponent', () => {
|
||||
PermissionsGroupComponent,
|
||||
IfOwnerDirective,
|
||||
ConfirmButtonComponent,
|
||||
DragDropSelectComponent,
|
||||
],
|
||||
providers: [CustomDatePipe, DatePipe, PermissionsGuard],
|
||||
imports: [
|
||||
@@ -108,6 +111,7 @@ describe('SettingsComponent', () => {
|
||||
NgSelectModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
NgbModalModule,
|
||||
DragDropModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
@@ -418,6 +422,7 @@ describe('SettingsComponent', () => {
|
||||
},
|
||||
}
|
||||
jest.spyOn(systemStatusService, 'get').mockReturnValue(of(status))
|
||||
jest.spyOn(permissionsService, 'isAdmin').mockReturnValue(true)
|
||||
completeSetup()
|
||||
expect(component['systemStatus']).toEqual(status) // private
|
||||
expect(component.systemStatusHasErrors).toBeTruthy()
|
||||
@@ -436,4 +441,11 @@ describe('SettingsComponent', () => {
|
||||
size: 'xl',
|
||||
})
|
||||
})
|
||||
|
||||
it('should support reset', () => {
|
||||
completeSetup()
|
||||
component.settingsForm.get('themeColor').setValue('#ff0000')
|
||||
component.reset()
|
||||
expect(component.settingsForm.get('themeColor').value).toEqual('')
|
||||
})
|
||||
})
|
||||
|
@@ -50,6 +50,7 @@ import {
|
||||
SystemStatusItemStatus,
|
||||
SystemStatus,
|
||||
} from 'src/app/data/system-status'
|
||||
import { DisplayMode } from 'src/app/data/document'
|
||||
|
||||
enum SettingsNavIDs {
|
||||
General = 1,
|
||||
@@ -73,8 +74,8 @@ export class SettingsComponent
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit, AfterViewInit, OnDestroy, DirtyComponent
|
||||
{
|
||||
SettingsNavIDs = SettingsNavIDs
|
||||
activeNavID: number
|
||||
DisplayMode = DisplayMode
|
||||
|
||||
savedViewGroup = new FormGroup({})
|
||||
|
||||
@@ -110,6 +111,10 @@ export class SettingsComponent
|
||||
})
|
||||
|
||||
savedViews: SavedView[]
|
||||
SettingsNavIDs = SettingsNavIDs
|
||||
get displayFields() {
|
||||
return this.settings.allDisplayFields
|
||||
}
|
||||
|
||||
store: BehaviorSubject<any>
|
||||
storeSub: Subscription
|
||||
@@ -121,7 +126,7 @@ export class SettingsComponent
|
||||
users: User[]
|
||||
groups: Group[]
|
||||
|
||||
private systemStatus: SystemStatus
|
||||
public systemStatus: SystemStatus
|
||||
|
||||
get systemStatusHasErrors(): boolean {
|
||||
return (
|
||||
@@ -340,6 +345,9 @@ export class SettingsComponent
|
||||
name: view.name,
|
||||
show_on_dashboard: view.show_on_dashboard,
|
||||
show_in_sidebar: view.show_in_sidebar,
|
||||
page_size: view.page_size,
|
||||
display_mode: view.display_mode,
|
||||
display_fields: view.display_fields,
|
||||
}
|
||||
this.savedViewGroup.addControl(
|
||||
view.id.toString(),
|
||||
@@ -348,6 +356,9 @@ export class SettingsComponent
|
||||
name: new FormControl(null),
|
||||
show_on_dashboard: new FormControl(null),
|
||||
show_in_sidebar: new FormControl(null),
|
||||
page_size: new FormControl(null),
|
||||
display_mode: new FormControl(null),
|
||||
display_fields: new FormControl([]),
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -385,12 +396,7 @@ export class SettingsComponent
|
||||
this.settingsForm.patchValue(currentFormValue)
|
||||
}
|
||||
|
||||
if (
|
||||
this.permissionsService.currentUserCan(
|
||||
PermissionAction.View,
|
||||
PermissionType.Admin
|
||||
)
|
||||
) {
|
||||
if (this.permissionsService.isAdmin()) {
|
||||
this.systemStatusService.get().subscribe((status) => {
|
||||
this.systemStatus = status
|
||||
})
|
||||
@@ -535,8 +541,8 @@ export class SettingsComponent
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.store.next(this.settingsForm.value)
|
||||
this.documentListViewService.updatePageSize()
|
||||
this.settings.updateAppearanceSettings()
|
||||
this.settings.initializeDisplayFields()
|
||||
let savedToast: Toast = {
|
||||
content: $localize`Settings were saved successfully.`,
|
||||
delay: 5000,
|
||||
@@ -597,6 +603,10 @@ export class SettingsComponent
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.settingsForm.patchValue(this.store.getValue())
|
||||
}
|
||||
|
||||
clearThemeColor() {
|
||||
this.settingsForm.get('themeColor').patchValue('')
|
||||
}
|
||||
|
@@ -52,40 +52,38 @@
|
||||
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Add Group</ng-container>
|
||||
</button>
|
||||
</h4>
|
||||
@if (groups.length > 0) {
|
||||
<ul class="list-group">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col"></div>
|
||||
<div class="col"></div>
|
||||
<div class="col" i18n>Actions</div>
|
||||
</div>
|
||||
</li>
|
||||
@for (group of groups; track group) {
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editGroup(group)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.Group)">{{group.name}}</button></div>
|
||||
<div class="col"></div>
|
||||
<div class="col"></div>
|
||||
<div class="col" i18n>Actions</div>
|
||||
</div>
|
||||
</li>
|
||||
@for (group of groups; track group) {
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editGroup(group)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.Group)">{{group.name}}</button></div>
|
||||
<div class="col"></div>
|
||||
<div class="col"></div>
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Group }">
|
||||
<i-bs width="1em" height="1em" name="pencil"></i-bs> <ng-container i18n>Edit</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Group }">
|
||||
<i-bs width="1em" height="1em" name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Group }">
|
||||
<i-bs width="1em" height="1em" name="pencil"></i-bs> <ng-container i18n>Edit</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Group }">
|
||||
<i-bs width="1em" height="1em" name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
@if (groups.length === 0) {
|
||||
<li class="list-group-item" i18n>No groups defined</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
@if (groups.length === 0) {
|
||||
<li class="list-group-item" i18n>No groups defined</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
|
||||
@if (!users || !groups) {
|
||||
|
@@ -111,7 +111,7 @@
|
||||
</h6>
|
||||
}
|
||||
<ul class="nav flex-column mb-2" cdkDropList (cdkDropListDropped)="onDrop($event)">
|
||||
@for (view of savedViewService.sidebarViews; track view) {
|
||||
@for (view of savedViewService.sidebarViews; track view.id) {
|
||||
<li class="nav-item w-100 app-link" cdkDrag [cdkDragDisabled]="!settingsService.organizingSidebarSavedViews"
|
||||
cdkDragPreviewContainer="parent" cdkDragPreviewClass="navItemDrag" (cdkDragStarted)="onDragStart($event)"
|
||||
(cdkDragEnded)="onDragEnd($event)">
|
||||
@@ -267,13 +267,15 @@
|
||||
}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }">
|
||||
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Logs"
|
||||
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="text-left"></i-bs><span> <ng-container i18n>Logs</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
@if (permissionsService.isAdmin()) {
|
||||
<li class="nav-item app-link">
|
||||
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Logs"
|
||||
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="text-left"></i-bs><span> <ng-container i18n>Logs</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
<li class="nav-item mt-2" tourAnchor="tour.outro">
|
||||
<a class="px-3 py-2 text-muted small d-flex align-items-center flex-wrap text-decoration-none"
|
||||
target="_blank" rel="noopener noreferrer" href="https://docs.paperless-ngx.com" ngbPopover="Documentation"
|
||||
|
@@ -36,14 +36,18 @@ describe('MergeConfirmDialogComponent', () => {
|
||||
{ id: 2, name: 'Document 2' },
|
||||
{ id: 3, name: 'Document 3' },
|
||||
]
|
||||
jest.spyOn(documentService, 'getCachedMany').mockReturnValue(of(documents))
|
||||
jest.spyOn(documentService, 'getFew').mockReturnValue(
|
||||
of({
|
||||
all: documents.map((d) => d.id),
|
||||
count: documents.length,
|
||||
results: documents,
|
||||
})
|
||||
)
|
||||
|
||||
component.ngOnInit()
|
||||
|
||||
expect(component.documents).toEqual(documents)
|
||||
expect(documentService.getCachedMany).toHaveBeenCalledWith(
|
||||
component.documentIDs
|
||||
)
|
||||
expect(documentService.getFew).toHaveBeenCalledWith(component.documentIDs)
|
||||
})
|
||||
|
||||
it('should move documentIDs on drop', () => {
|
||||
@@ -64,7 +68,13 @@ describe('MergeConfirmDialogComponent', () => {
|
||||
{ id: 2, name: 'Document 2' },
|
||||
{ id: 3, name: 'Document 3' },
|
||||
]
|
||||
jest.spyOn(documentService, 'getCachedMany').mockReturnValue(of(documents))
|
||||
jest.spyOn(documentService, 'getFew').mockReturnValue(
|
||||
of({
|
||||
all: documents.map((d) => d.id),
|
||||
count: documents.length,
|
||||
results: documents,
|
||||
})
|
||||
)
|
||||
|
||||
component.ngOnInit()
|
||||
|
||||
|
@@ -34,10 +34,10 @@ export class MergeConfirmDialogComponent
|
||||
|
||||
ngOnInit() {
|
||||
this.documentService
|
||||
.getCachedMany(this.documentIDs)
|
||||
.getFew(this.documentIDs)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((documents) => {
|
||||
this._documents = documents
|
||||
.subscribe((r) => {
|
||||
this._documents = r.results
|
||||
})
|
||||
}
|
||||
|
||||
|
@@ -12,7 +12,7 @@
|
||||
</div>
|
||||
<div class="col-8 d-flex align-items-center">
|
||||
@if (documentID) {
|
||||
<img class="w-50 m-auto" [ngStyle]="{'transform': 'rotate('+rotation+'deg)'}" [src]="documentService.getThumbUrl(documentID)" />
|
||||
<img class="w-75 m-auto" [ngStyle]="{'transform': 'rotate('+rotation+'deg)'}" [src]="documentService.getThumbUrl(documentID)" />
|
||||
}
|
||||
</div>
|
||||
<div class="col-2 d-flex">
|
||||
|
@@ -6,7 +6,7 @@
|
||||
<div class="modal-body">
|
||||
<p>{{message}}</p>
|
||||
<div class="row mb-2">
|
||||
<div class="col-6">
|
||||
<div class="col-8">
|
||||
<div class="input-group input-group-sm">
|
||||
<div class="input-group-text" i18n>Page</div>
|
||||
<input class="form-control" type="number" min="1" [(ngModel)]="page" />
|
||||
@@ -21,9 +21,9 @@
|
||||
</pngx-pdf-viewer>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="col-4">
|
||||
<div class="d-grid">
|
||||
<button class="btn btn-sm btn-primary" (click)="addSplit()">
|
||||
<button class="btn btn-sm btn-primary" (click)="addSplit()" [disabled]="page === totalPages">
|
||||
<i-bs name="plus-circle"></i-bs>
|
||||
<span i18n>Add Split</span>
|
||||
</button>
|
||||
@@ -31,11 +31,11 @@
|
||||
|
||||
<ul class="list-group mt-3">
|
||||
@for (pageStr of pagesString.split(','); track pageStr; let i = $index) {
|
||||
<li class="list-group-item">
|
||||
<li class="list-group-item d-flex align-items-center">
|
||||
{{pageStr}}
|
||||
@if (pagesString.split(',').length > 1) {
|
||||
|
||||
<button class="btn btn-sm btn-danger" (click)="removeSplit(i)">
|
||||
<button class="btn btn-sm btn-danger ms-auto" (click)="removeSplit(i)">
|
||||
<i-bs name="trash"></i-bs>
|
||||
</button>
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
.pdf-viewer-container {
|
||||
background-color: gray;
|
||||
height: 300px;
|
||||
height: 350px;
|
||||
|
||||
pngx-pdf-viewer {
|
||||
width: 100%;
|
||||
|
@@ -0,0 +1,25 @@
|
||||
@if (field) {
|
||||
@switch (field.data_type) {
|
||||
@case (CustomFieldDataType.Monetary) {
|
||||
<span>{{value | currency: currency}}</span>
|
||||
}
|
||||
@case (CustomFieldDataType.Date) {
|
||||
<span>{{value | customDate}}</span>
|
||||
}
|
||||
@case (CustomFieldDataType.Url) {
|
||||
<a [href]="value" class="btn-link text-dark text-decoration-none" target="_blank">{{value}}</a>
|
||||
}
|
||||
@case (CustomFieldDataType.DocumentLink) {
|
||||
<div class="d-flex gap-1 flex-wrap">
|
||||
@for (docId of value; track docId) {
|
||||
<a routerLink="/documents/{{docId}}" class="badge bg-dark text-primary" title="View" i18n-title>
|
||||
<i-bs width="0.9em" height="0.9em" name="file-text"></i-bs> <span>{{ getDocumentTitle(docId) }}</span>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@default {
|
||||
<span>{{value}}</span>
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,89 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { of } from 'rxjs'
|
||||
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { CustomFieldDisplayComponent } from './custom-field-display.component'
|
||||
import { DisplayField, Document } from 'src/app/data/document'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
|
||||
const customFields: CustomField[] = [
|
||||
{ id: 1, name: 'Field 1', data_type: CustomFieldDataType.String },
|
||||
{ id: 2, name: 'Field 2', data_type: CustomFieldDataType.Monetary },
|
||||
{ id: 3, name: 'Field 3', data_type: CustomFieldDataType.DocumentLink },
|
||||
]
|
||||
const document: Document = {
|
||||
id: 1,
|
||||
title: 'Doc 1',
|
||||
custom_fields: [
|
||||
{ field: 1, document: 1, created: null, value: 'Text value' },
|
||||
{ field: 2, document: 1, created: null, value: '100 USD' },
|
||||
{ field: 3, document: 1, created: null, value: '1,2,3' },
|
||||
],
|
||||
}
|
||||
|
||||
describe('CustomFieldDisplayComponent', () => {
|
||||
let component: CustomFieldDisplayComponent
|
||||
let fixture: ComponentFixture<CustomFieldDisplayComponent>
|
||||
let documentService: DocumentService
|
||||
let customFieldService: CustomFieldsService
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [CustomFieldDisplayComponent],
|
||||
providers: [DocumentService],
|
||||
imports: [HttpClientTestingModule],
|
||||
}).compileComponents()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
documentService = TestBed.inject(DocumentService)
|
||||
customFieldService = TestBed.inject(CustomFieldsService)
|
||||
jest
|
||||
.spyOn(customFieldService, 'listAll')
|
||||
.mockReturnValue(of({ results: customFields } as any))
|
||||
fixture = TestBed.createComponent(CustomFieldDisplayComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should initialize component', () => {
|
||||
jest
|
||||
.spyOn(documentService, 'getFew')
|
||||
.mockReturnValue(of({ results: [] } as any))
|
||||
|
||||
component.fieldDisplayKey = DisplayField.CUSTOM_FIELD + '2'
|
||||
expect(component.fieldId).toEqual(2)
|
||||
component.document = document
|
||||
expect(component.document.title).toEqual('Doc 1')
|
||||
|
||||
expect(component.field).toEqual(customFields[1])
|
||||
expect(component.value).toEqual(100)
|
||||
expect(component.currency).toEqual('USD')
|
||||
})
|
||||
|
||||
it('should get document titles', () => {
|
||||
const docLinkDocuments: Document[] = [
|
||||
{ id: 1, title: 'Document 1' } as any,
|
||||
{ id: 2, title: 'Document 2' } as any,
|
||||
{ id: 3, title: 'Document 3' } as any,
|
||||
]
|
||||
jest
|
||||
.spyOn(documentService, 'getFew')
|
||||
.mockReturnValue(of({ results: docLinkDocuments } as any))
|
||||
component.fieldId = 3
|
||||
component.document = document
|
||||
|
||||
const title1 = component.getDocumentTitle(1)
|
||||
const title2 = component.getDocumentTitle(2)
|
||||
const title3 = component.getDocumentTitle(3)
|
||||
|
||||
expect(title1).toEqual('Document 1')
|
||||
expect(title2).toEqual('Document 2')
|
||||
expect(title3).toEqual('Document 3')
|
||||
})
|
||||
})
|
@@ -0,0 +1,105 @@
|
||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core'
|
||||
import { Subject, takeUntil } from 'rxjs'
|
||||
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||
import { DisplayField, Document } from 'src/app/data/document'
|
||||
import { Results } from 'src/app/data/results'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-custom-field-display',
|
||||
templateUrl: './custom-field-display.component.html',
|
||||
styleUrl: './custom-field-display.component.scss',
|
||||
})
|
||||
export class CustomFieldDisplayComponent implements OnInit, OnDestroy {
|
||||
CustomFieldDataType = CustomFieldDataType
|
||||
|
||||
private _document: Document
|
||||
@Input()
|
||||
set document(document: Document) {
|
||||
this._document = document
|
||||
this.init()
|
||||
}
|
||||
|
||||
get document(): Document {
|
||||
return this._document
|
||||
}
|
||||
|
||||
private _fieldId: number
|
||||
@Input()
|
||||
set fieldId(id: number) {
|
||||
this._fieldId = id
|
||||
this.init()
|
||||
}
|
||||
|
||||
get fieldId(): number {
|
||||
return this._fieldId
|
||||
}
|
||||
|
||||
@Input()
|
||||
set fieldDisplayKey(key: string) {
|
||||
this.fieldId = parseInt(key.replace(DisplayField.CUSTOM_FIELD, ''), 10)
|
||||
}
|
||||
|
||||
value: any
|
||||
currency: string
|
||||
|
||||
private customFields: CustomField[] = []
|
||||
|
||||
public field: CustomField
|
||||
|
||||
private docLinkDocuments: Document[] = []
|
||||
|
||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||
|
||||
constructor(
|
||||
private customFieldService: CustomFieldsService,
|
||||
private documentService: DocumentService
|
||||
) {
|
||||
this.customFieldService.listAll().subscribe((r) => {
|
||||
this.customFields = r.results
|
||||
this.init()
|
||||
})
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.init()
|
||||
}
|
||||
|
||||
private init() {
|
||||
if (this.value || !this._fieldId || !this._document || !this.customFields) {
|
||||
return
|
||||
}
|
||||
this.field = this.customFields.find((f) => f.id === this._fieldId)
|
||||
this.value = this._document.custom_fields.find(
|
||||
(f) => f.field === this._fieldId
|
||||
)?.value
|
||||
if (this.value && this.field.data_type === CustomFieldDataType.Monetary) {
|
||||
this.currency = this.value.match(/([A-Z]{3})/)?.[0]
|
||||
this.value = parseFloat(this.value.replace(this.currency, ''))
|
||||
} else if (
|
||||
this.value?.length &&
|
||||
this.field.data_type === CustomFieldDataType.DocumentLink
|
||||
) {
|
||||
this.getDocuments()
|
||||
}
|
||||
}
|
||||
|
||||
private getDocuments() {
|
||||
this.documentService
|
||||
.getFew(this.value, { fields: 'id,title' })
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((result: Results<Document>) => {
|
||||
this.docLinkDocuments = result.results
|
||||
})
|
||||
}
|
||||
|
||||
public getDocumentTitle(docId: number): string {
|
||||
return this.docLinkDocuments?.find((d) => d.id === docId)?.title
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.unsubscribeNotifier.next(true)
|
||||
this.unsubscribeNotifier.complete()
|
||||
}
|
||||
}
|
@@ -6,7 +6,7 @@
|
||||
<div ngbDropdownMenu aria-labelledby="customFieldsDropdown" class="shadow custom-fields-dropdown">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<pngx-input-select class="mb-3"
|
||||
<pngx-input-select
|
||||
[items]="unusedFields"
|
||||
bindLabel="name"
|
||||
[(ngModel)]="field"
|
||||
@@ -14,14 +14,15 @@
|
||||
[notFoundText]="notFoundText"
|
||||
[disableCreateNew]="!canCreateFields"
|
||||
(createNew)="createField($event)"
|
||||
[hideAddButton]="true"
|
||||
bindValue="id">
|
||||
</pngx-input-select>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<button class="btn btn-sm btn-outline-secondary me-auto" type="button" (click)="createField()" [disabled]="!canCreateFields">
|
||||
<i-bs width="1em" height="1em" name="asterisk"></i-bs> <ng-container i18n>Create New Field</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary me-1" type="button" (click)="addField(); fieldDropdown.close()" [disabled]="field === undefined">
|
||||
<i-bs width="1.2em" height="1.2em" name="plus-circle"></i-bs> <ng-container i18n>Add</ng-container>
|
||||
<button class="btn btn-sm btn-outline-primary" type="button" (click)="addField(); fieldDropdown.close()" [disabled]="field === undefined">
|
||||
<i-bs width="1.2em" height="1.2em" name="plus-circle"></i-bs> <ng-container i18n>Add to document</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
.custom-fields-dropdown {
|
||||
min-width: 350px;
|
||||
min-width: 380px;
|
||||
|
||||
// correct position on mobile
|
||||
@media (max-width: 575.98px) {
|
||||
@@ -9,16 +9,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .ng-select .ng-select-container .ng-value-container .ng-placeholder,
|
||||
::ng-deep .ng-select .ng-option,
|
||||
::ng-deep .ng-select .ng-select-container .ng-value-container .ng-value {
|
||||
::ng-deep .custom-fields-dropdown .ng-select .ng-select-container .ng-value-container .ng-placeholder,
|
||||
::ng-deep .custom-fields-dropdown .ng-select .ng-option,
|
||||
::ng-deep .custom-fields-dropdown .ng-select .ng-select-container .ng-value-container .ng-value {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
::ng-deep .paperless-input-select .ng-select {
|
||||
min-height: calc(1em + 0.75rem + 5px);
|
||||
}
|
||||
|
||||
::ng-deep .paperless-input-select .ng-select .ng-select-container .ng-value-container .ng-input {
|
||||
::ng-deep .custom-fields-dropdown .paperless-input-select .ng-select .ng-select-container .ng-value-container .ng-input {
|
||||
top: 4px;
|
||||
}
|
||||
|
@@ -1,10 +1,7 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
|
||||
import { CustomFieldsDropdownComponent } from './custom-fields-dropdown.component'
|
||||
import {
|
||||
HttpClientTestingModule,
|
||||
HttpTestingController,
|
||||
} from '@angular/common/http/testing'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { of } from 'rxjs'
|
||||
|
@@ -1,71 +0,0 @@
|
||||
<div class="btn-group w-100" ngbDropdown role="group">
|
||||
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="dateBefore || dateAfter ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled">
|
||||
{{title}}
|
||||
<pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
|
||||
</button>
|
||||
<div class="dropdown-menu date-dropdown shadow pt-0" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
|
||||
<div class="list-group list-group-flush">
|
||||
@for (rd of relativeDates; track rd) {
|
||||
<button class="list-group-item small list-goup list-group-item-action d-flex p-2" role="menuitem" (click)="setRelativeDate(rd.id)">
|
||||
<div class="selected-icon">
|
||||
@if (relativeDate === rd.id) {
|
||||
<i-bs width="1em" height="1em" name="check"></i-bs>
|
||||
}
|
||||
</div>
|
||||
<div class="d-flex justify-content-between w-100 align-items-center ps-2">
|
||||
<div class="pe-2 pe-lg-4">
|
||||
{{rd.name}}
|
||||
</div>
|
||||
<div class="text-muted small pe-2">
|
||||
<span class="small">
|
||||
{{ rd.date | customDate:'mediumDate' }} – <ng-container i18n>now</ng-container>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
|
||||
|
||||
<div class="mb-2 d-flex flex-row w-100 justify-content-between small">
|
||||
<div i18n>After</div>
|
||||
@if (dateAfter) {
|
||||
<a class="btn btn-link p-0 m-0" (click)="clearAfter()">
|
||||
<i-bs width="1em" height="1em" name="x"></i-bs>
|
||||
<small i18n>Clear</small>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="input-group input-group-sm">
|
||||
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||
maxlength="10" [(ngModel)]="dateAfter" ngbDatepicker #dateAfterPicker="ngbDatepicker">
|
||||
<button class="btn btn-outline-secondary" (click)="dateAfterPicker.toggle()" type="button">
|
||||
<i-bs width="1em" height="1em" name="calendar"></i-bs>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
|
||||
|
||||
<div class="mb-2 d-flex flex-row w-100 justify-content-between small">
|
||||
<div i18n>Before</div>
|
||||
@if (dateBefore) {
|
||||
<a class="btn btn-link p-0 m-0" (click)="clearBefore()">
|
||||
<i-bs width="1em" height="1em" name="x"></i-bs>
|
||||
<small i18n>Clear</small>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="input-group input-group-sm">
|
||||
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||
maxlength="10" [(ngModel)]="dateBefore" ngbDatepicker #dateBeforePicker="ngbDatepicker">
|
||||
<button class="btn btn-outline-secondary" (click)="dateBeforePicker.toggle()" type="button">
|
||||
<i-bs width="1em" height="1em" name="calendar"></i-bs>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -1,164 +0,0 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
} from '@angular/core'
|
||||
import { NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Subject, Subscription } from 'rxjs'
|
||||
import { debounceTime } from 'rxjs/operators'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
|
||||
|
||||
export interface DateSelection {
|
||||
before?: string
|
||||
after?: string
|
||||
relativeDateID?: number
|
||||
}
|
||||
|
||||
export enum RelativeDate {
|
||||
LAST_7_DAYS = 0,
|
||||
LAST_MONTH = 1,
|
||||
LAST_3_MONTHS = 2,
|
||||
LAST_YEAR = 3,
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-date-dropdown',
|
||||
templateUrl: './date-dropdown.component.html',
|
||||
styleUrls: ['./date-dropdown.component.scss'],
|
||||
providers: [{ provide: NgbDateAdapter, useClass: ISODateAdapter }],
|
||||
})
|
||||
export class DateDropdownComponent implements OnInit, OnDestroy {
|
||||
constructor(settings: SettingsService) {
|
||||
this.datePlaceHolder = settings.getLocalizedDateInputFormat()
|
||||
}
|
||||
|
||||
relativeDates = [
|
||||
{
|
||||
id: RelativeDate.LAST_7_DAYS,
|
||||
name: $localize`Last 7 days`,
|
||||
date: new Date().setDate(new Date().getDate() - 7),
|
||||
},
|
||||
{
|
||||
id: RelativeDate.LAST_MONTH,
|
||||
name: $localize`Last month`,
|
||||
date: new Date().setMonth(new Date().getMonth() - 1),
|
||||
},
|
||||
{
|
||||
id: RelativeDate.LAST_3_MONTHS,
|
||||
name: $localize`Last 3 months`,
|
||||
date: new Date().setMonth(new Date().getMonth() - 3),
|
||||
},
|
||||
{
|
||||
id: RelativeDate.LAST_YEAR,
|
||||
name: $localize`Last year`,
|
||||
date: new Date().setFullYear(new Date().getFullYear() - 1),
|
||||
},
|
||||
]
|
||||
|
||||
datePlaceHolder: string
|
||||
|
||||
@Input()
|
||||
dateBefore: string
|
||||
|
||||
@Output()
|
||||
dateBeforeChange = new EventEmitter<string>()
|
||||
|
||||
@Input()
|
||||
dateAfter: string
|
||||
|
||||
@Output()
|
||||
dateAfterChange = new EventEmitter<string>()
|
||||
|
||||
@Input()
|
||||
relativeDate: RelativeDate
|
||||
|
||||
@Output()
|
||||
relativeDateChange = new EventEmitter<number>()
|
||||
|
||||
@Input()
|
||||
title: string
|
||||
|
||||
@Output()
|
||||
datesSet = new EventEmitter<DateSelection>()
|
||||
|
||||
@Input()
|
||||
disabled: boolean = false
|
||||
|
||||
get isActive(): boolean {
|
||||
return (
|
||||
this.relativeDate !== null ||
|
||||
this.dateAfter?.length > 0 ||
|
||||
this.dateBefore?.length > 0
|
||||
)
|
||||
}
|
||||
|
||||
private datesSetDebounce$ = new Subject()
|
||||
|
||||
private sub: Subscription
|
||||
|
||||
ngOnInit() {
|
||||
this.sub = this.datesSetDebounce$.pipe(debounceTime(400)).subscribe(() => {
|
||||
this.onChange()
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.sub) {
|
||||
this.sub.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.dateBefore = null
|
||||
this.dateAfter = null
|
||||
this.relativeDate = null
|
||||
this.onChange()
|
||||
}
|
||||
|
||||
setRelativeDate(rd: RelativeDate) {
|
||||
this.dateBefore = null
|
||||
this.dateAfter = null
|
||||
this.relativeDate = this.relativeDate == rd ? null : rd
|
||||
this.onChange()
|
||||
}
|
||||
|
||||
onChange() {
|
||||
this.dateBeforeChange.emit(this.dateBefore)
|
||||
this.dateAfterChange.emit(this.dateAfter)
|
||||
this.relativeDateChange.emit(this.relativeDate)
|
||||
this.datesSet.emit({
|
||||
after: this.dateAfter,
|
||||
before: this.dateBefore,
|
||||
relativeDateID: this.relativeDate,
|
||||
})
|
||||
}
|
||||
|
||||
onChangeDebounce() {
|
||||
this.relativeDate = null
|
||||
this.datesSetDebounce$.next({
|
||||
after: this.dateAfter,
|
||||
before: this.dateBefore,
|
||||
})
|
||||
}
|
||||
|
||||
clearBefore() {
|
||||
this.dateBefore = null
|
||||
this.onChange()
|
||||
}
|
||||
|
||||
clearAfter() {
|
||||
this.dateAfter = null
|
||||
this.onChange()
|
||||
}
|
||||
|
||||
// prevent chars other than numbers and separators
|
||||
onKeyPress(event: KeyboardEvent) {
|
||||
if ('Enter' !== event.key && !/[0-9,\.\/-]+/.test(event.key)) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,143 @@
|
||||
<div class="btn-group w-100" ngbDropdown role="group">
|
||||
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="createdDateBefore || createdDateAfter ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled">
|
||||
<i-bs width="1em" height="1em" name="calendar-event-fill"></i-bs>
|
||||
<div class="d-none d-sm-inline"> {{title}}</div>
|
||||
<pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
|
||||
</button>
|
||||
<div class="dropdown-menu date-dropdown shadow p-2" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
|
||||
<div class="row d-flex">
|
||||
<div class="col border-end">
|
||||
<div class="list-group list-group-flush">
|
||||
<h6 class="dropdown-header border-bottom" i18n>Created</h6>
|
||||
@for (rd of relativeDates; track rd) {
|
||||
<button class="list-group-item small list-goup list-group-item-action d-flex p-2" role="menuitem" (click)="setCreatedRelativeDate(rd.id)">
|
||||
<div class="selected-icon">
|
||||
@if (createdRelativeDate === rd.id) {
|
||||
<i-bs width="1em" height="1em" name="check"></i-bs>
|
||||
}
|
||||
</div>
|
||||
<div class="d-flex justify-content-between w-100 align-items-center ps-2">
|
||||
<div class="pe-2 pe-lg-4">
|
||||
{{rd.name}}
|
||||
</div>
|
||||
<div class="text-muted small pe-2">
|
||||
<span class="small">
|
||||
{{ rd.date | customDate:'mediumDate' }} – <ng-container i18n>now</ng-container>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
|
||||
|
||||
<div class="mb-2 d-flex flex-row w-100 justify-content-between small">
|
||||
<div i18n>After</div>
|
||||
@if (createdDateAfter) {
|
||||
<a class="btn btn-link p-0 m-0" (click)="clearCreatedAfter()">
|
||||
<i-bs width="1em" height="1em" name="x"></i-bs>
|
||||
<small i18n>Clear</small>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="input-group input-group-sm">
|
||||
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||
maxlength="10" [(ngModel)]="createdDateAfter" ngbDatepicker #createdDateAfterPicker="ngbDatepicker">
|
||||
<button class="btn btn-outline-secondary" (click)="createdDateAfterPicker.toggle()" type="button">
|
||||
<i-bs width="1em" height="1em" name="calendar"></i-bs>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
|
||||
|
||||
<div class="mb-2 d-flex flex-row w-100 justify-content-between small">
|
||||
<div i18n>Before</div>
|
||||
@if (createdDateBefore) {
|
||||
<a class="btn btn-link p-0 m-0" (click)="clearCreatedBefore()">
|
||||
<i-bs width="1em" height="1em" name="x"></i-bs>
|
||||
<small i18n>Clear</small>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="input-group input-group-sm">
|
||||
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||
maxlength="10" [(ngModel)]="createdDateBefore" ngbDatepicker #createdDateBeforePicker="ngbDatepicker">
|
||||
<button class="btn btn-outline-secondary" (click)="createdDateBeforePicker.toggle()" type="button">
|
||||
<i-bs width="1em" height="1em" name="calendar"></i-bs>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h6 class="dropdown-header border-bottom" i18n>Added</h6>
|
||||
<div class="list-group list-group-flush">
|
||||
@for (rd of relativeDates; track rd) {
|
||||
<button class="list-group-item small list-goup list-group-item-action d-flex p-2" role="menuitem" (click)="setAddedRelativeDate(rd.id)">
|
||||
<div class="selected-icon">
|
||||
@if (addedRelativeDate === rd.id) {
|
||||
<i-bs width="1em" height="1em" name="check"></i-bs>
|
||||
}
|
||||
</div>
|
||||
<div class="d-flex justify-content-between w-100 align-items-center ps-2">
|
||||
<div class="pe-2 pe-lg-4">
|
||||
{{rd.name}}
|
||||
</div>
|
||||
<div class="text-muted small pe-2">
|
||||
<span class="small">
|
||||
{{ rd.date | customDate:'mediumDate' }} – <ng-container i18n>now</ng-container>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
|
||||
|
||||
<div class="mb-2 d-flex flex-row w-100 justify-content-between small">
|
||||
<div i18n>After</div>
|
||||
@if (addedDateAfter) {
|
||||
<a class="btn btn-link p-0 m-0" (click)="clearAddedAfter()">
|
||||
<i-bs width="1em" height="1em" name="x"></i-bs>
|
||||
<small i18n>Clear</small>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="input-group input-group-sm">
|
||||
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||
maxlength="10" [(ngModel)]="addedDateAfter" ngbDatepicker #addedDateAfterPicker="ngbDatepicker">
|
||||
<button class="btn btn-outline-secondary" (click)="addedDateAfterPicker.toggle()" type="button">
|
||||
<i-bs width="1em" height="1em" name="calendar"></i-bs>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
|
||||
|
||||
<div class="mb-2 d-flex flex-row w-100 justify-content-between small">
|
||||
<div i18n>Before</div>
|
||||
@if (addedDateBefore) {
|
||||
<a class="btn btn-link p-0 m-0" (click)="clearAddedBefore()">
|
||||
<i-bs width="1em" height="1em" name="x"></i-bs>
|
||||
<small i18n>Clear</small>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="input-group input-group-sm">
|
||||
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||
maxlength="10" [(ngModel)]="addedDateBefore" ngbDatepicker #addedDateBeforePicker="ngbDatepicker">
|
||||
<button class="btn btn-outline-secondary" (click)="addedDateBeforePicker.toggle()" type="button">
|
||||
<i-bs width="1em" height="1em" name="calendar"></i-bs>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -1,6 +1,10 @@
|
||||
.date-dropdown {
|
||||
white-space: nowrap;
|
||||
|
||||
@media(min-width: 768px) {
|
||||
--bs-dropdown-min-width: 40rem;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
line-height: 1;
|
||||
}
|
@@ -4,12 +4,12 @@ import {
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
let fixture: ComponentFixture<DateDropdownComponent>
|
||||
let fixture: ComponentFixture<DatesDropdownComponent>
|
||||
import {
|
||||
DateDropdownComponent,
|
||||
DatesDropdownComponent,
|
||||
DateSelection,
|
||||
RelativeDate,
|
||||
} from './date-dropdown.component'
|
||||
} from './dates-dropdown.component'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
@@ -19,15 +19,15 @@ import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { DatePipe } from '@angular/common'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
|
||||
describe('DateDropdownComponent', () => {
|
||||
let component: DateDropdownComponent
|
||||
describe('DatesDropdownComponent', () => {
|
||||
let component: DatesDropdownComponent
|
||||
let settingsService: SettingsService
|
||||
let settingsSpy
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
DateDropdownComponent,
|
||||
DatesDropdownComponent,
|
||||
ClearableBadgeComponent,
|
||||
CustomDatePipe,
|
||||
],
|
||||
@@ -44,7 +44,7 @@ describe('DateDropdownComponent', () => {
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
settingsSpy = jest.spyOn(settingsService, 'getLocalizedDateInputFormat')
|
||||
|
||||
fixture = TestBed.createComponent(DateDropdownComponent)
|
||||
fixture = TestBed.createComponent(DatesDropdownComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
@@ -57,7 +57,7 @@ describe('DateDropdownComponent', () => {
|
||||
|
||||
it('should support date input, emit change', fakeAsync(() => {
|
||||
let result: string
|
||||
component.dateAfterChange.subscribe((date) => (result = date))
|
||||
component.createdDateAfterChange.subscribe((date) => (result = date))
|
||||
const input: HTMLInputElement = fixture.nativeElement.querySelector('input')
|
||||
input.value = '5/30/2023'
|
||||
input.dispatchEvent(new Event('change'))
|
||||
@@ -78,45 +78,69 @@ describe('DateDropdownComponent', () => {
|
||||
it('should support relative dates', fakeAsync(() => {
|
||||
let result: DateSelection
|
||||
component.datesSet.subscribe((date) => (result = date))
|
||||
component.setRelativeDate(null)
|
||||
component.setRelativeDate(RelativeDate.LAST_7_DAYS)
|
||||
component.setCreatedRelativeDate(null)
|
||||
component.setCreatedRelativeDate(RelativeDate.LAST_7_DAYS)
|
||||
component.setAddedRelativeDate(null)
|
||||
component.setAddedRelativeDate(RelativeDate.LAST_7_DAYS)
|
||||
tick(500)
|
||||
expect(result).toEqual({
|
||||
after: null,
|
||||
before: null,
|
||||
relativeDateID: RelativeDate.LAST_7_DAYS,
|
||||
createdAfter: null,
|
||||
createdBefore: null,
|
||||
createdRelativeDateID: RelativeDate.LAST_7_DAYS,
|
||||
addedAfter: null,
|
||||
addedBefore: null,
|
||||
addedRelativeDateID: RelativeDate.LAST_7_DAYS,
|
||||
})
|
||||
}))
|
||||
|
||||
it('should support report if active', () => {
|
||||
component.relativeDate = RelativeDate.LAST_7_DAYS
|
||||
component.createdRelativeDate = RelativeDate.LAST_7_DAYS
|
||||
expect(component.isActive).toBeTruthy()
|
||||
component.relativeDate = null
|
||||
component.dateAfter = '2023-05-30'
|
||||
component.createdRelativeDate = null
|
||||
component.createdDateAfter = '2023-05-30'
|
||||
expect(component.isActive).toBeTruthy()
|
||||
component.dateAfter = null
|
||||
component.dateBefore = '2023-05-30'
|
||||
component.createdDateAfter = null
|
||||
component.createdDateBefore = '2023-05-30'
|
||||
expect(component.isActive).toBeTruthy()
|
||||
component.dateBefore = null
|
||||
component.createdDateBefore = null
|
||||
|
||||
component.addedRelativeDate = RelativeDate.LAST_7_DAYS
|
||||
expect(component.isActive).toBeTruthy()
|
||||
component.addedRelativeDate = null
|
||||
component.addedDateAfter = '2023-05-30'
|
||||
expect(component.isActive).toBeTruthy()
|
||||
component.addedDateAfter = null
|
||||
component.addedDateBefore = '2023-05-30'
|
||||
expect(component.isActive).toBeTruthy()
|
||||
component.addedDateBefore = null
|
||||
|
||||
expect(component.isActive).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should support reset', () => {
|
||||
component.dateAfter = '2023-05-30'
|
||||
component.createdDateAfter = '2023-05-30'
|
||||
component.reset()
|
||||
expect(component.dateAfter).toBeNull()
|
||||
expect(component.createdDateAfter).toBeNull()
|
||||
})
|
||||
|
||||
it('should support clearAfter', () => {
|
||||
component.dateAfter = '2023-05-30'
|
||||
component.clearAfter()
|
||||
expect(component.dateAfter).toBeNull()
|
||||
component.createdDateAfter = '2023-05-30'
|
||||
component.clearCreatedAfter()
|
||||
expect(component.createdDateAfter).toBeNull()
|
||||
|
||||
component.addedDateAfter = '2023-05-30'
|
||||
component.clearAddedAfter()
|
||||
expect(component.addedDateAfter).toBeNull()
|
||||
})
|
||||
|
||||
it('should support clearBefore', () => {
|
||||
component.dateBefore = '2023-05-30'
|
||||
component.clearBefore()
|
||||
expect(component.dateBefore).toBeNull()
|
||||
component.createdDateBefore = '2023-05-30'
|
||||
component.clearCreatedBefore()
|
||||
expect(component.createdDateBefore).toBeNull()
|
||||
|
||||
component.addedDateBefore = '2023-05-30'
|
||||
component.clearAddedBefore()
|
||||
expect(component.addedDateBefore).toBeNull()
|
||||
})
|
||||
|
||||
it('should limit keyboard events', () => {
|
@@ -0,0 +1,219 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
} from '@angular/core'
|
||||
import { NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Subject, Subscription } from 'rxjs'
|
||||
import { debounceTime } from 'rxjs/operators'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
|
||||
|
||||
export interface DateSelection {
|
||||
createdBefore?: string
|
||||
createdAfter?: string
|
||||
createdRelativeDateID?: number
|
||||
addedBefore?: string
|
||||
addedAfter?: string
|
||||
addedRelativeDateID?: number
|
||||
}
|
||||
|
||||
export enum RelativeDate {
|
||||
LAST_7_DAYS = 0,
|
||||
LAST_MONTH = 1,
|
||||
LAST_3_MONTHS = 2,
|
||||
LAST_YEAR = 3,
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-dates-dropdown',
|
||||
templateUrl: './dates-dropdown.component.html',
|
||||
styleUrls: ['./dates-dropdown.component.scss'],
|
||||
providers: [{ provide: NgbDateAdapter, useClass: ISODateAdapter }],
|
||||
})
|
||||
export class DatesDropdownComponent implements OnInit, OnDestroy {
|
||||
constructor(settings: SettingsService) {
|
||||
this.datePlaceHolder = settings.getLocalizedDateInputFormat()
|
||||
}
|
||||
|
||||
relativeDates = [
|
||||
{
|
||||
id: RelativeDate.LAST_7_DAYS,
|
||||
name: $localize`Last 7 days`,
|
||||
date: new Date().setDate(new Date().getDate() - 7),
|
||||
},
|
||||
{
|
||||
id: RelativeDate.LAST_MONTH,
|
||||
name: $localize`Last month`,
|
||||
date: new Date().setMonth(new Date().getMonth() - 1),
|
||||
},
|
||||
{
|
||||
id: RelativeDate.LAST_3_MONTHS,
|
||||
name: $localize`Last 3 months`,
|
||||
date: new Date().setMonth(new Date().getMonth() - 3),
|
||||
},
|
||||
{
|
||||
id: RelativeDate.LAST_YEAR,
|
||||
name: $localize`Last year`,
|
||||
date: new Date().setFullYear(new Date().getFullYear() - 1),
|
||||
},
|
||||
]
|
||||
|
||||
datePlaceHolder: string
|
||||
|
||||
// created
|
||||
@Input()
|
||||
createdDateBefore: string
|
||||
|
||||
@Output()
|
||||
createdDateBeforeChange = new EventEmitter<string>()
|
||||
|
||||
@Input()
|
||||
createdDateAfter: string
|
||||
|
||||
@Output()
|
||||
createdDateAfterChange = new EventEmitter<string>()
|
||||
|
||||
@Input()
|
||||
createdRelativeDate: RelativeDate
|
||||
|
||||
@Output()
|
||||
createdRelativeDateChange = new EventEmitter<number>()
|
||||
|
||||
// added
|
||||
@Input()
|
||||
addedDateBefore: string
|
||||
|
||||
@Output()
|
||||
addedDateBeforeChange = new EventEmitter<string>()
|
||||
|
||||
@Input()
|
||||
addedDateAfter: string
|
||||
|
||||
@Output()
|
||||
addedDateAfterChange = new EventEmitter<string>()
|
||||
|
||||
@Input()
|
||||
addedRelativeDate: RelativeDate
|
||||
|
||||
@Output()
|
||||
addedRelativeDateChange = new EventEmitter<number>()
|
||||
|
||||
@Input()
|
||||
title: string
|
||||
|
||||
@Output()
|
||||
datesSet = new EventEmitter<DateSelection>()
|
||||
|
||||
@Input()
|
||||
disabled: boolean = false
|
||||
|
||||
get isActive(): boolean {
|
||||
return (
|
||||
this.createdRelativeDate !== null ||
|
||||
this.createdDateAfter?.length > 0 ||
|
||||
this.createdDateBefore?.length > 0 ||
|
||||
this.addedRelativeDate !== null ||
|
||||
this.addedDateAfter?.length > 0 ||
|
||||
this.addedDateBefore?.length > 0
|
||||
)
|
||||
}
|
||||
|
||||
private datesSetDebounce$ = new Subject()
|
||||
|
||||
private sub: Subscription
|
||||
|
||||
ngOnInit() {
|
||||
this.sub = this.datesSetDebounce$.pipe(debounceTime(400)).subscribe(() => {
|
||||
this.onChange()
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.sub) {
|
||||
this.sub.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.createdDateBefore = null
|
||||
this.createdDateAfter = null
|
||||
this.createdRelativeDate = null
|
||||
this.addedDateBefore = null
|
||||
this.addedDateAfter = null
|
||||
this.addedRelativeDate = null
|
||||
this.onChange()
|
||||
}
|
||||
|
||||
setCreatedRelativeDate(rd: RelativeDate) {
|
||||
this.createdDateBefore = null
|
||||
this.createdDateAfter = null
|
||||
this.createdRelativeDate = this.createdRelativeDate == rd ? null : rd
|
||||
this.onChange()
|
||||
}
|
||||
|
||||
setAddedRelativeDate(rd: RelativeDate) {
|
||||
this.addedDateBefore = null
|
||||
this.addedDateAfter = null
|
||||
this.addedRelativeDate = this.addedRelativeDate == rd ? null : rd
|
||||
this.onChange()
|
||||
}
|
||||
|
||||
onChange() {
|
||||
this.createdDateBeforeChange.emit(this.createdDateBefore)
|
||||
this.createdDateAfterChange.emit(this.createdDateAfter)
|
||||
this.createdRelativeDateChange.emit(this.createdRelativeDate)
|
||||
this.addedDateBeforeChange.emit(this.addedDateBefore)
|
||||
this.addedDateAfterChange.emit(this.addedDateAfter)
|
||||
this.addedRelativeDateChange.emit(this.addedRelativeDate)
|
||||
this.datesSet.emit({
|
||||
createdAfter: this.createdDateAfter,
|
||||
createdBefore: this.createdDateBefore,
|
||||
createdRelativeDateID: this.createdRelativeDate,
|
||||
addedAfter: this.addedDateAfter,
|
||||
addedBefore: this.addedDateBefore,
|
||||
addedRelativeDateID: this.addedRelativeDate,
|
||||
})
|
||||
}
|
||||
|
||||
onChangeDebounce() {
|
||||
this.createdRelativeDate = null
|
||||
this.addedRelativeDate = null
|
||||
this.datesSetDebounce$.next({
|
||||
createdAfter: this.createdDateAfter,
|
||||
createdBefore: this.createdDateBefore,
|
||||
addedAfter: this.addedDateAfter,
|
||||
addedBefore: this.addedDateBefore,
|
||||
})
|
||||
}
|
||||
|
||||
clearCreatedBefore() {
|
||||
this.createdDateBefore = null
|
||||
this.onChange()
|
||||
}
|
||||
|
||||
clearCreatedAfter() {
|
||||
this.createdDateAfter = null
|
||||
this.onChange()
|
||||
}
|
||||
|
||||
clearAddedBefore() {
|
||||
this.addedDateBefore = null
|
||||
this.onChange()
|
||||
}
|
||||
|
||||
clearAddedAfter() {
|
||||
this.addedDateAfter = null
|
||||
this.onChange()
|
||||
}
|
||||
|
||||
// prevent chars other than numbers and separators
|
||||
onKeyPress(event: KeyboardEvent) {
|
||||
if ('Enter' !== event.key && !/[0-9,\.\/-]+/.test(event.key)) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
@@ -16,11 +16,15 @@
|
||||
<pngx-input-text i18n-title title="First name" formControlName="first_name" [error]="error?.first_name"></pngx-input-text>
|
||||
<pngx-input-text i18n-title title="Last name" formControlName="last_name" [error]="error?.first_name"></pngx-input-text>
|
||||
|
||||
<div class="mb-2">
|
||||
<div class="mb-2 d-flex flex-column">
|
||||
<div class="form-check form-switch form-check-inline">
|
||||
<input type="checkbox" class="form-check-input" id="is_active" formControlName="is_active">
|
||||
<label class="form-check-label" for="is_active" i18n>Active</label>
|
||||
</div>
|
||||
<div class="form-check form-switch form-check-inline">
|
||||
<input type="checkbox" class="form-check-input" id="is_staff" formControlName="is_staff">
|
||||
<label class="form-check-label" for="is_staff"><ng-container i18n>Admin</ng-container> <small class="form-text text-muted ms-1" i18n>Access logs, Django backend</small></label>
|
||||
</div>
|
||||
<div class="form-check form-switch form-check-inline">
|
||||
<input type="checkbox" class="form-check-input" id="is_superuser" formControlName="is_superuser" (change)="onToggleSuperUser()">
|
||||
<label class="form-check-label" for="is_superuser"><ng-container i18n>Superuser</ng-container> <small class="form-text text-muted ms-1" i18n>(Grants all permissions and can view objects)</small></label>
|
||||
|
@@ -56,6 +56,7 @@ export class UserEditDialogComponent
|
||||
first_name: new FormControl(''),
|
||||
last_name: new FormControl(''),
|
||||
is_active: new FormControl(true),
|
||||
is_staff: new FormControl(true),
|
||||
is_superuser: new FormControl(false),
|
||||
groups: new FormControl([]),
|
||||
user_permissions: new FormControl([]),
|
||||
|
@@ -0,0 +1,26 @@
|
||||
<div class="d-flex flex-row mt-2 align-items-center">
|
||||
<span class="me-2">{{title}}:</span>
|
||||
<div class="d-flex flex-row gap-2 w-100 mh-1" style="min-height: 1em;"
|
||||
cdkDropList #selectedList="cdkDropList"
|
||||
cdkDropListOrientation="horizontal"
|
||||
(cdkDropListDropped)="drop($event)"
|
||||
[cdkDropListConnectedTo]="[unselectedList]">
|
||||
@for (item of selectedItems; track item.id) {
|
||||
<div class="badge bg-primary" cdkDrag>{{item.name}}</div>
|
||||
}
|
||||
@if (selectedItems.length === 0) {
|
||||
<div class="badge bg-light fst-italic" i18n>{{emptyText}}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-row mt-2 align-items-center bg-light p-2">
|
||||
<div class="d-flex flex-row gap-2 w-100 mh-1" style="min-height: 1em;"
|
||||
cdkDropList #unselectedList="cdkDropList"
|
||||
cdkDropListOrientation="horizontal"
|
||||
(cdkDropListDropped)="drop($event)"
|
||||
[cdkDropListConnectedTo]="[selectedList]">
|
||||
@for (item of unselectedItems; track item.id) {
|
||||
<div class="badge bg-secondary opacity-50" cdkDrag>{{item.name}}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,7 @@
|
||||
.badge {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.d-flex {
|
||||
overflow-x: scroll;
|
||||
}
|
@@ -0,0 +1,102 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop'
|
||||
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { DragDropSelectComponent } from './drag-drop-select.component'
|
||||
|
||||
describe('DragDropSelectComponent', () => {
|
||||
let component: DragDropSelectComponent
|
||||
let fixture: ComponentFixture<DragDropSelectComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [DragDropModule, FormsModule],
|
||||
declarations: [DragDropSelectComponent],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(DragDropSelectComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should update selectedItems when writeValue is called', () => {
|
||||
const newValue = ['1', '2', '3']
|
||||
component.items = [
|
||||
{ id: '1', name: 'Item 1' },
|
||||
{ id: '2', name: 'Item 2' },
|
||||
{ id: '3', name: 'Item 3' },
|
||||
]
|
||||
component.writeValue(newValue)
|
||||
expect(component.selectedItems).toEqual([
|
||||
{ id: '1', name: 'Item 1' },
|
||||
{ id: '2', name: 'Item 2' },
|
||||
{ id: '3', name: 'Item 3' },
|
||||
])
|
||||
|
||||
component.writeValue(null)
|
||||
expect(component.selectedItems).toEqual([])
|
||||
})
|
||||
|
||||
it('should update selectedItems when an item is dropped within selectedList', () => {
|
||||
component.items = [
|
||||
{ id: '1', name: 'Item 1' },
|
||||
{ id: '2', name: 'Item 2' },
|
||||
{ id: '3', name: 'Item 3' },
|
||||
{ id: '4', name: 'Item 4' },
|
||||
]
|
||||
component.writeValue(['1', '2', '3'])
|
||||
const event = {
|
||||
previousContainer: component.selectedList,
|
||||
container: component.selectedList,
|
||||
previousIndex: 1,
|
||||
currentIndex: 2,
|
||||
}
|
||||
component.drop(event as any)
|
||||
expect(component.selectedItems).toEqual([
|
||||
{ id: '1', name: 'Item 1' },
|
||||
{ id: '3', name: 'Item 3' },
|
||||
{ id: '2', name: 'Item 2' },
|
||||
])
|
||||
})
|
||||
|
||||
it('should update selectedItems when an item is dropped from unselectedList to selectedList', () => {
|
||||
component.items = [
|
||||
{ id: '1', name: 'Item 1' },
|
||||
{ id: '2', name: 'Item 2' },
|
||||
{ id: '3', name: 'Item 3' },
|
||||
]
|
||||
component.writeValue(['1', '2'])
|
||||
const event = {
|
||||
previousContainer: component.unselectedList,
|
||||
container: component.selectedList,
|
||||
previousIndex: 0,
|
||||
currentIndex: 2,
|
||||
}
|
||||
component.drop(event as any)
|
||||
expect(component.selectedItems).toEqual([
|
||||
{ id: '1', name: 'Item 1' },
|
||||
{ id: '2', name: 'Item 2' },
|
||||
{ id: '3', name: 'Item 3' },
|
||||
])
|
||||
})
|
||||
|
||||
it('should update selectedItems when an item is dropped from selectedList to unselectedList', () => {
|
||||
component.items = [
|
||||
{ id: '1', name: 'Item 1' },
|
||||
{ id: '2', name: 'Item 2' },
|
||||
{ id: '3', name: 'Item 3' },
|
||||
]
|
||||
component.writeValue(['1', '2', '3'])
|
||||
const event = {
|
||||
previousContainer: component.selectedList,
|
||||
container: component.unselectedList,
|
||||
previousIndex: 1,
|
||||
currentIndex: 0,
|
||||
}
|
||||
component.drop(event as any)
|
||||
expect(component.selectedItems).toEqual([
|
||||
{ id: '1', name: 'Item 1' },
|
||||
{ id: '3', name: 'Item 3' },
|
||||
])
|
||||
})
|
||||
})
|
@@ -0,0 +1,68 @@
|
||||
import { Component, Input, ViewChild, forwardRef } from '@angular/core'
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { AbstractInputComponent } from '../abstract-input'
|
||||
import {
|
||||
CdkDragDrop,
|
||||
CdkDropList,
|
||||
moveItemInArray,
|
||||
} from '@angular/cdk/drag-drop'
|
||||
|
||||
@Component({
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => DragDropSelectComponent),
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
selector: 'pngx-input-drag-drop-select',
|
||||
templateUrl: './drag-drop-select.component.html',
|
||||
styleUrl: './drag-drop-select.component.scss',
|
||||
})
|
||||
export class DragDropSelectComponent extends AbstractInputComponent<string[]> {
|
||||
@Input() title: string = $localize`Selected items`
|
||||
|
||||
@Input() items: { id: string; name: string }[] = []
|
||||
public selectedItems: { id: string; name: string }[] = []
|
||||
|
||||
@Input()
|
||||
emptyText = $localize`No items selected`
|
||||
|
||||
@ViewChild('selectedList') selectedList: CdkDropList
|
||||
@ViewChild('unselectedList') unselectedList: CdkDropList
|
||||
|
||||
get unselectedItems(): { id: string; name: string }[] {
|
||||
return this.items.filter((i) => !this.selectedItems.includes(i))
|
||||
}
|
||||
|
||||
writeValue(newValue: string[]): void {
|
||||
super.writeValue(newValue)
|
||||
this.selectedItems =
|
||||
newValue?.map((id) => this.items.find((i) => i.id === id)) ?? []
|
||||
}
|
||||
|
||||
public drop(event: CdkDragDrop<string[]>) {
|
||||
if (
|
||||
event.previousContainer === event.container &&
|
||||
event.container === this.selectedList
|
||||
) {
|
||||
moveItemInArray(
|
||||
this.selectedItems,
|
||||
event.previousIndex,
|
||||
event.currentIndex
|
||||
)
|
||||
} else if (event.container === this.selectedList) {
|
||||
this.selectedItems.splice(
|
||||
event.currentIndex,
|
||||
0,
|
||||
this.unselectedItems[event.previousIndex]
|
||||
)
|
||||
} else if (
|
||||
event.container === this.unselectedList &&
|
||||
event.previousContainer === this.selectedList
|
||||
) {
|
||||
this.selectedItems.splice(event.previousIndex, 1)
|
||||
}
|
||||
this.onChange(this.selectedItems.map((i) => i.id))
|
||||
}
|
||||
}
|
@@ -12,9 +12,9 @@
|
||||
</div>
|
||||
<div class="position-relative" [class.col-md-9]="horizontal">
|
||||
<div class="input-group" [class.is-invalid]="error">
|
||||
<span class="input-group-text fw-bold bg-light">{{monetaryValue | currency: currencyCode }}</span>
|
||||
<input #currencyField class="form-control text-muted mw-60" tabindex="0" [(ngModel)]="currencyCode" maxlength="3" [class.is-invalid]="error" (change)="onChange(value)" [disabled]="disabled">
|
||||
<input #inputField type="number" tabindex="0" class="form-control text-muted" step=".01" [id]="inputId" [(ngModel)]="monetaryValue" (change)="onChange(value)" [class.is-invalid]="error" [disabled]="disabled">
|
||||
<span class="input-group-text fw-bold bg-light">{{ monetaryValue | currency: currency }}</span>
|
||||
<input #currencyField class="form-control text-muted mw-60" [(ngModel)]="currency" (input)="currencyChange()" maxlength="3" [class.is-invalid]="error" [disabled]="disabled">
|
||||
<input #monetaryValueField type="number" class="form-control text-muted" step=".01" [(ngModel)]="monetaryValue" (input)="monetaryValueChange()" (change)="monetaryValueChange(true)" [class.is-invalid]="error" [disabled]="disabled">
|
||||
</div>
|
||||
<div class="invalid-feedback position-absolute top-100">
|
||||
{{error}}
|
||||
|
@@ -11,7 +11,6 @@ import { MonetaryComponent } from './monetary.component'
|
||||
describe('MonetaryComponent', () => {
|
||||
let component: MonetaryComponent
|
||||
let fixture: ComponentFixture<MonetaryComponent>
|
||||
let input: HTMLInputElement
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
@@ -24,37 +23,22 @@ describe('MonetaryComponent', () => {
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
input = component.inputField.nativeElement
|
||||
})
|
||||
|
||||
it('should set the currency code correctly', () => {
|
||||
expect(component.currencyCode).toEqual('USD') // default
|
||||
component.currencyCode = 'EUR'
|
||||
expect(component.currencyCode).toEqual('EUR')
|
||||
it('should set the currency code and monetary value correctly', () => {
|
||||
expect(component.currency).toEqual('USD') // default
|
||||
component.writeValue('G123.4')
|
||||
expect(component.currency).toEqual('G')
|
||||
|
||||
component.value = 'G123.4'
|
||||
jest
|
||||
.spyOn(document, 'activeElement', 'get')
|
||||
.mockReturnValue(component.currencyField.nativeElement)
|
||||
expect(component.currencyCode).toEqual('G')
|
||||
component.writeValue('EUR123.4')
|
||||
expect(component.currency).toEqual('EUR')
|
||||
expect(component.monetaryValue).toEqual('123.40')
|
||||
})
|
||||
|
||||
it('should parse monetary value only when out of focus', () => {
|
||||
component.monetaryValue = 10.5
|
||||
jest.spyOn(document, 'activeElement', 'get').mockReturnValue(null)
|
||||
it('should set monetary value to fixed decimals', () => {
|
||||
component.monetaryValue = '10.5'
|
||||
component.monetaryValueChange(true)
|
||||
expect(component.monetaryValue).toEqual('10.50')
|
||||
|
||||
component.value = 'GBP123.4'
|
||||
jest
|
||||
.spyOn(document, 'activeElement', 'get')
|
||||
.mockReturnValue(component.inputField.nativeElement)
|
||||
expect(component.monetaryValue).toEqual('123.4')
|
||||
})
|
||||
|
||||
it('should report value including currency code and monetary value', () => {
|
||||
component.currencyCode = 'EUR'
|
||||
component.monetaryValue = 10.5
|
||||
expect(component.value).toEqual('EUR10.50')
|
||||
})
|
||||
|
||||
it('should set the default currency code based on LOCALE_ID', () => {
|
||||
@@ -62,4 +46,32 @@ describe('MonetaryComponent', () => {
|
||||
component = new MonetaryComponent('pt-BR')
|
||||
expect(component.defaultCurrencyCode).toEqual('BRL')
|
||||
})
|
||||
|
||||
it('should parse monetary value correctly', () => {
|
||||
expect(component['parseMonetaryValue']('123.4')).toEqual('123.4')
|
||||
expect(component['parseMonetaryValue']('123.4', true)).toEqual('123.40')
|
||||
expect(component['parseMonetaryValue']('123.4', false)).toEqual('123.4')
|
||||
})
|
||||
|
||||
it('should handle currency change', () => {
|
||||
component.writeValue('USD123.4')
|
||||
component.currency = 'EUR'
|
||||
component.currencyChange()
|
||||
expect(component.currency).toEqual('EUR')
|
||||
expect(component.monetaryValue).toEqual('123.40')
|
||||
})
|
||||
|
||||
it('should handle monetary value change', () => {
|
||||
component.writeValue('USD123.4')
|
||||
component.monetaryValue = '123.4'
|
||||
component.monetaryValueChange()
|
||||
expect(component.monetaryValue).toEqual('123.4')
|
||||
expect(component.value).toEqual('USD123.40')
|
||||
})
|
||||
|
||||
it('should handle null values', () => {
|
||||
component.writeValue(null)
|
||||
expect(component.currency).toEqual('USD')
|
||||
expect(component.monetaryValue).toEqual('')
|
||||
})
|
||||
})
|
||||
|
@@ -1,11 +1,4 @@
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
forwardRef,
|
||||
Inject,
|
||||
LOCALE_ID,
|
||||
ViewChild,
|
||||
} from '@angular/core'
|
||||
import { Component, forwardRef, Inject, LOCALE_ID } from '@angular/core'
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { AbstractInputComponent } from '../abstract-input'
|
||||
import { getLocaleCurrencyCode } from '@angular/common'
|
||||
@@ -23,39 +16,50 @@ import { getLocaleCurrencyCode } from '@angular/common'
|
||||
styleUrls: ['./monetary.component.scss'],
|
||||
})
|
||||
export class MonetaryComponent extends AbstractInputComponent<string> {
|
||||
@ViewChild('currencyField')
|
||||
currencyField: ElementRef
|
||||
public currency: string = ''
|
||||
public monetaryValue: string = ''
|
||||
defaultCurrencyCode: string
|
||||
|
||||
constructor(@Inject(LOCALE_ID) currentLocale: string) {
|
||||
super()
|
||||
|
||||
this.defaultCurrencyCode = getLocaleCurrencyCode(currentLocale)
|
||||
this.currency = this.defaultCurrencyCode =
|
||||
getLocaleCurrencyCode(currentLocale)
|
||||
}
|
||||
|
||||
get currencyCode(): string {
|
||||
const focused = document.activeElement === this.currencyField?.nativeElement
|
||||
if (focused && this.value) return this.value.match(/^([A-Z]{0,3})/)?.[0]
|
||||
writeValue(newValue: any): void {
|
||||
this.currency = this.parseCurrencyCode(newValue)
|
||||
this.monetaryValue = this.parseMonetaryValue(newValue, true)
|
||||
|
||||
this.value = this.currency + this.monetaryValue
|
||||
}
|
||||
|
||||
public monetaryValueChange(fixed: boolean = false): void {
|
||||
this.monetaryValue = this.parseMonetaryValue(this.monetaryValue, fixed)
|
||||
this.onChange(this.currency + this.monetaryValue)
|
||||
}
|
||||
|
||||
public currencyChange(): void {
|
||||
if (this.currency.length) {
|
||||
this.currency = this.parseCurrencyCode(this.currency)
|
||||
this.onChange(this.currency + this.monetaryValue?.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private parseCurrencyCode(value: string): string {
|
||||
return (
|
||||
this.value
|
||||
value
|
||||
?.toString()
|
||||
.toUpperCase()
|
||||
.match(/^([A-Z]{1,3})/)?.[0] ?? this.defaultCurrencyCode
|
||||
)
|
||||
}
|
||||
|
||||
set currencyCode(value: string) {
|
||||
this.value = value + this.monetaryValue?.toString()
|
||||
}
|
||||
|
||||
get monetaryValue(): string {
|
||||
if (!this.value) return null
|
||||
const focused = document.activeElement === this.inputField?.nativeElement
|
||||
const val = parseFloat(this.value.toString().replace(/[^0-9.,]+/g, ''))
|
||||
return focused ? val.toString() : val.toFixed(2)
|
||||
}
|
||||
|
||||
set monetaryValue(value: number) {
|
||||
this.value = this.currencyCode + value.toFixed(2)
|
||||
private parseMonetaryValue(value: string, fixed: boolean = false): string {
|
||||
if (!value) {
|
||||
return ''
|
||||
}
|
||||
const val: number = parseFloat(value.toString().replace(/[^0-9.,-]+/g, ''))
|
||||
return fixed ? val.toFixed(2) : val.toString()
|
||||
}
|
||||
}
|
||||
|
@@ -36,7 +36,7 @@
|
||||
<span [title]="item[bindLabel]">{{item[bindLabel]}}</span>
|
||||
</ng-template>
|
||||
</ng-select>
|
||||
@if (allowCreateNew) {
|
||||
@if (allowCreateNew && !hideAddButton) {
|
||||
<button class="btn btn-outline-secondary" type="button" (click)="addItem()" [disabled]="disabled">
|
||||
<i-bs width="1.2em" height="1.2em" name="plus"></i-bs>
|
||||
</button>
|
||||
|
@@ -94,6 +94,9 @@ export class SelectComponent extends AbstractInputComponent<number> {
|
||||
@Input()
|
||||
disableCreateNew: boolean = false
|
||||
|
||||
@Input()
|
||||
hideAddButton: boolean = false
|
||||
|
||||
@Output()
|
||||
createNew = new EventEmitter<string>()
|
||||
|
||||
|
@@ -9,17 +9,17 @@
|
||||
<div class="col" i18n>Delete</div>
|
||||
<div class="col" i18n>View</div>
|
||||
</li>
|
||||
@for (type of PermissionType | keyvalue; track type) {
|
||||
<li class="list-group-item d-flex" [formGroupName]="type.key">
|
||||
<div class="col-3">{{type.key}}:</div>
|
||||
<div class="col form-check form-check-inline form-switch" [ngbPopover]="inheritedWarning" [disablePopover]="!isInherited(type.key)" placement="left" triggers="mouseenter:mouseleave">
|
||||
<input type="checkbox" class="form-check-input" id="{{type.key}}_all" (change)="toggleAll($event, type.key)" [checked]="typesWithAllActions.has(type.key) || isInherited(type.key)" [attr.disabled]="disabled || isInherited(type.key) ? true : null">
|
||||
<label class="form-check-label visually-hidden" for="{{type.key}}_all" i18n>All</label>
|
||||
@for (type of allowedTypes; track type) {
|
||||
<li class="list-group-item d-flex" [formGroupName]="type">
|
||||
<div class="col-3">{{type}}:</div>
|
||||
<div class="col form-check form-check-inline form-switch" [ngbPopover]="inheritedWarning" [disablePopover]="!isInherited(type)" placement="left" triggers="mouseenter:mouseleave">
|
||||
<input type="checkbox" class="form-check-input" id="{{type}}_all" (change)="toggleAll($event, type)" [checked]="typesWithAllActions.has(type) || isInherited(type)" [attr.disabled]="disabled || isInherited(type) ? true : null">
|
||||
<label class="form-check-label visually-hidden" for="{{type}}_all" i18n>All</label>
|
||||
</div>
|
||||
@for (action of PermissionAction | keyvalue; track action) {
|
||||
<div class="col form-check form-check-inline" [ngbPopover]="inheritedWarning" [disablePopover]="!isInherited(type.key, action.key)" placement="left" triggers="mouseenter:mouseleave">
|
||||
<input type="checkbox" class="form-check-input" id="{{type.key}}_{{action.key}}" formControlName="{{action.key}}">
|
||||
<label class="form-check-label visually-hidden" for="{{type.key}}_{{action.key}}" i18n>{{action.key}}</label>
|
||||
<div class="col form-check form-check-inline" [ngbPopover]="inheritedWarning" [disablePopover]="!isInherited(type, action.key)" placement="left" triggers="mouseenter:mouseleave">
|
||||
<input type="checkbox" class="form-check-input" id="{{type}}_{{action.key}}" formControlName="{{action.key}}">
|
||||
<label class="form-check-label visually-hidden" for="{{type}}_{{action.key}}" i18n>{{action.key}}</label>
|
||||
</div>
|
||||
}
|
||||
</li>
|
||||
|
@@ -12,6 +12,9 @@ import {
|
||||
} from 'src/app/services/permissions.service'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
|
||||
const permissions = [
|
||||
'add_document',
|
||||
@@ -28,6 +31,7 @@ describe('PermissionsSelectComponent', () => {
|
||||
let component: PermissionsSelectComponent
|
||||
let fixture: ComponentFixture<PermissionsSelectComponent>
|
||||
let permissionsChangeResult: Permissions
|
||||
let settingsService: SettingsService
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
@@ -38,9 +42,11 @@ describe('PermissionsSelectComponent', () => {
|
||||
ReactiveFormsModule,
|
||||
NgbModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
HttpClientTestingModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
fixture = TestBed.createComponent(PermissionsSelectComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
@@ -99,4 +105,11 @@ describe('PermissionsSelectComponent', () => {
|
||||
const input2 = fixture.debugElement.query(By.css('input#Tag_Change'))
|
||||
expect(input2.nativeElement.disabled).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should exclude history permissions if disabled', () => {
|
||||
settingsService.set(SETTINGS_KEYS.AUDITLOG_ENABLED, false)
|
||||
fixture = TestBed.createComponent(PermissionsSelectComponent)
|
||||
component = fixture.componentInstance
|
||||
expect(component.allowedTypes).not.toContain('History')
|
||||
})
|
||||
})
|
||||
|
@@ -12,6 +12,8 @@ import {
|
||||
PermissionType,
|
||||
} from 'src/app/services/permissions.service'
|
||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
|
||||
@Component({
|
||||
providers: [
|
||||
@@ -60,15 +62,23 @@ export class PermissionsSelectComponent
|
||||
|
||||
inheritedWarning: string = $localize`Inherited from group`
|
||||
|
||||
constructor(private readonly permissionsService: PermissionsService) {
|
||||
public allowedTypes = Object.keys(PermissionType)
|
||||
|
||||
constructor(
|
||||
private readonly permissionsService: PermissionsService,
|
||||
private readonly settingsService: SettingsService
|
||||
) {
|
||||
super()
|
||||
for (const type in PermissionType) {
|
||||
if (!this.settingsService.get(SETTINGS_KEYS.AUDITLOG_ENABLED)) {
|
||||
this.allowedTypes.splice(this.allowedTypes.indexOf('History'), 1)
|
||||
}
|
||||
this.allowedTypes.forEach((type) => {
|
||||
const control = new FormGroup({})
|
||||
for (const action in PermissionAction) {
|
||||
control.addControl(action, new FormControl(null))
|
||||
}
|
||||
this.form.addControl(type, control)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
writeValue(permissions: string[]): void {
|
||||
@@ -92,7 +102,7 @@ export class PermissionsSelectComponent
|
||||
}
|
||||
}
|
||||
})
|
||||
Object.keys(PermissionType).forEach((type) => {
|
||||
this.allowedTypes.forEach((type) => {
|
||||
if (
|
||||
Object.values(this.form.get(type).value).every((val) => val == true)
|
||||
) {
|
||||
@@ -191,7 +201,7 @@ export class PermissionsSelectComponent
|
||||
}
|
||||
|
||||
updateDisabledStates() {
|
||||
for (const type in PermissionType) {
|
||||
this.allowedTypes.forEach((type) => {
|
||||
const control = this.form.get(type)
|
||||
let actionControl: AbstractControl
|
||||
for (const action in PermissionAction) {
|
||||
@@ -200,6 +210,6 @@ export class PermissionsSelectComponent
|
||||
? actionControl.disable()
|
||||
: actionControl.enable()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -23,7 +23,7 @@
|
||||
}
|
||||
|
||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
|
||||
@for (v of dashboardViews; track v) {
|
||||
@for (v of dashboardViews; track v.id) {
|
||||
<div class="col">
|
||||
<pngx-saved-view-widget
|
||||
[savedView]="v"
|
||||
|
@@ -9,58 +9,114 @@
|
||||
<a class="btn-link text-decoration-none" header-buttons [routerLink]="[]" (click)="showAll()" i18n>Show all</a>
|
||||
}
|
||||
|
||||
@if (documents.length) {
|
||||
<table content class="table table-hover mb-0 align-middle">
|
||||
@if (documents.length && displayMode === DisplayMode.TABLE) {
|
||||
<table content class="table table-hover mb-0 mt-n2 align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" i18n>Created</th>
|
||||
<th scope="col" i18n>Title</th>
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Tag)) {
|
||||
<th scope="col" class="d-none d-md-table-cell" i18n>Tags</th>
|
||||
}
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
|
||||
<th scope="col" class="d-none d-md-table-cell" i18n>Correspondent</th>
|
||||
} @else {
|
||||
<th scope="col" class="d-none d-md-table-cell"></th>
|
||||
@for (field of displayFields; track field; let i = $index) {
|
||||
@if (displayFields.includes(field)) {
|
||||
<th
|
||||
scope="col"
|
||||
[ngClass]="{
|
||||
'd-none d-md-table-cell': i > 1,
|
||||
'w-25': field === DisplayField.CREATED || field === DisplayField.ADDED
|
||||
}">
|
||||
{{ getColumnTitle(field) }}
|
||||
</th>
|
||||
}
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (doc of documents; track doc) {
|
||||
<tr (mouseleave)="maybeClosePopover()">
|
||||
<td class="py-2 py-md-3"><a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.created_date | customDate}}</a></td>
|
||||
<td class="py-2 py-md-3">
|
||||
<a routerLink="/documents/{{doc.id}}" title="Edit" i18n-title class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.title | documentTitle}}</a>
|
||||
</td>
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Tag)) {
|
||||
<td class="py-2 py-md-3 d-none d-md-table-cell">
|
||||
@for (t of doc.tags$ | async; track t) {
|
||||
<pngx-tag [tag]="t" class="ms-1" (click)="clickTag(t, $event)"></pngx-tag>
|
||||
@for (doc of documents; track doc.id) {
|
||||
<tr>
|
||||
@for (field of displayFields; track field; let i = $index) {
|
||||
<td class="py-2 py-md-3 position-relative" [ngClass]="{ 'd-none d-md-table-cell': i > 1 }">
|
||||
@switch (field) {
|
||||
@case (DisplayField.ADDED) {
|
||||
<a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.added | customDate}}</a>
|
||||
}
|
||||
@case (DisplayField.CREATED) {
|
||||
<a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.created_date | customDate}}</a>
|
||||
}
|
||||
@case (DisplayField.TITLE) {
|
||||
<a routerLink="/documents/{{doc.id}}" title="Edit" i18n-title class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.title | documentTitle}}</a>
|
||||
}
|
||||
@case (DisplayField.CORRESPONDENT) {
|
||||
@if (doc.correspondent) {
|
||||
<a class="btn-link text-dark text-decoration-none" type="button" (click)="clickCorrespondent(doc.correspondent, $event)">{{(doc.correspondent$ | async)?.name}}</a>
|
||||
}
|
||||
}
|
||||
@case (DisplayField.TAGS) {
|
||||
@for (t of doc.tags$ | async; track t) {
|
||||
<pngx-tag [tag]="t" class="ms-1" (click)="clickTag(t.id, $event)"></pngx-tag>
|
||||
}
|
||||
}
|
||||
@case (DisplayField.DOCUMENT_TYPE) {
|
||||
@if (doc.document_type) {
|
||||
<a class="btn-link text-dark text-decoration-none" type="button" (click)="clickDocType(doc.document_type, $event)">{{(doc.document_type$ | async)?.name}}</a>
|
||||
}
|
||||
}
|
||||
@case (DisplayField.STORAGE_PATH) {
|
||||
@if (doc.storage_path) {
|
||||
<a class="btn-link text-dark text-decoration-none" type="button" (click)="clickStoragePath(doc.storage_path, $event)">{{(doc.storage_path$ | async)?.name}}</a>
|
||||
}
|
||||
}
|
||||
}
|
||||
@if (field.startsWith(DisplayField.CUSTOM_FIELD)) {
|
||||
<pngx-custom-field-display [document]="doc" [fieldDisplayKey]="field"></pngx-custom-field-display>
|
||||
}
|
||||
@if (i === displayFields.length - 1) {
|
||||
<div class="btn-group position-absolute top-50 end-0 translate-middle-y">
|
||||
<a [href]="getPreviewUrl(doc)" title="View Preview" i18n-title target="_blank" class="btn px-4 btn-dark border-dark-subtle"
|
||||
[ngbPopover]="previewContent" [popoverTitle]="doc.title | documentTitle"
|
||||
autoClose="true" popoverClass="shadow popover-preview" container="body" (mouseenter)="mouseEnterPreviewButton(doc)" (mouseleave)="mouseLeavePreviewButton()" #popover="ngbPopover">
|
||||
<i-bs width="0.8em" height="0.8em" name="eye"></i-bs>
|
||||
</a>
|
||||
<ng-template #previewContent>
|
||||
<pngx-preview-popup [document]="doc" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()"></pngx-preview-popup>
|
||||
</ng-template>
|
||||
<a [href]="getDownloadUrl(doc)" class="btn px-4 btn-dark border-dark-subtle" title="Download" i18n-title (click)="$event.stopPropagation()">
|
||||
<i-bs width="0.8em" height="0.8em" name="download"></i-bs>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
<td class="position-relative py-2 py-md-3 d-none d-md-table-cell">
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Correspondent) && doc.correspondent !== null) {
|
||||
<a class="btn-link text-dark text-decoration-none py-2 py-md-3" routerLink="/documents" [queryParams]="getCorrespondentQueryParams(doc.correspondent)">{{(doc.correspondent$ | async)?.name}}</a>
|
||||
}
|
||||
<div class="btn-group position-absolute top-50 end-0 translate-middle-y">
|
||||
<a [href]="getPreviewUrl(doc)" title="View Preview" i18n-title target="_blank" class="btn px-4 btn-dark border-dark-subtle"
|
||||
[ngbPopover]="previewContent" [popoverTitle]="doc.title | documentTitle"
|
||||
autoClose="true" popoverClass="shadow popover-preview" container="body" (mouseenter)="mouseEnterPreviewButton(doc)" (mouseleave)="mouseLeavePreviewButton()" #popover="ngbPopover">
|
||||
<i-bs width="0.8em" height="0.8em" name="eye"></i-bs>
|
||||
</a>
|
||||
<ng-template #previewContent>
|
||||
<pngx-preview-popup [document]="doc" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()"></pngx-preview-popup>
|
||||
</ng-template>
|
||||
<a [href]="getDownloadUrl(doc)" class="btn px-4 btn-dark border-dark-subtle" title="Download" i18n-title (click)="$event.stopPropagation()">
|
||||
<i-bs width="0.8em" height="0.8em" name="download"></i-bs>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
} @else if (documents.length && displayMode === DisplayMode.SMALL_CARDS) {
|
||||
<div class="row row-cols-paperless-cards my-n2">
|
||||
@for (d of documents; track d.id) {
|
||||
<pngx-document-card-small
|
||||
class="p-0"
|
||||
(dblClickDocument)="openDocumentDetail(d)"
|
||||
[document]="d"
|
||||
[displayFields]="displayFields"
|
||||
(clickTag)="clickTag($event)"
|
||||
(clickCorrespondent)="clickCorrespondent($event)"
|
||||
(clickStoragePath)="clickStoragePath($event)"
|
||||
(clickDocumentType)="clickDocumentType($event)">
|
||||
</pngx-document-card-small>
|
||||
}
|
||||
</div>
|
||||
} @else if (documents.length && displayMode === DisplayMode.LARGE_CARDS) {
|
||||
<div class="row my-n2">
|
||||
@for (d of documents; track d.id) {
|
||||
<pngx-document-card-large
|
||||
(dblClickDocument)="openDocumentDetail(d)"
|
||||
[document]="d"
|
||||
[displayFields]="displayFields"
|
||||
(clickTag)="clickTag($event)"
|
||||
(clickCorrespondent)="clickCorrespondent($event)"
|
||||
(clickStoragePath)="clickStoragePath($event)"
|
||||
(clickDocumentType)="clickDocumentType($event)"
|
||||
(clickMoreLike)="clickMoreLike(d.id)">
|
||||
</pngx-document-card-large>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<p i18n class="text-center text-muted mb-0 fst-italic">No documents</p>
|
||||
}
|
||||
|
@@ -3,10 +3,9 @@ table {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
th:first-child {
|
||||
width: 25%;
|
||||
@media (min-width: 768px) {
|
||||
width: 15%;
|
||||
@media (min-width: 768px) {
|
||||
th.w-25 {
|
||||
width: 15% !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,3 +29,45 @@ td.py-3 {
|
||||
padding-top: 0.75em !important;
|
||||
padding-bottom: 0.75em !important;
|
||||
}
|
||||
|
||||
$paperless-card-breakpoints: (
|
||||
// 0: 2, // xs is manual for slim-sidebar
|
||||
768px: 2, //md
|
||||
992px: 2, //lg
|
||||
1200px: 3, //xl
|
||||
1600px: 4,
|
||||
1800px: 5,
|
||||
2000px: 6
|
||||
);
|
||||
|
||||
.row-cols-paperless-cards {
|
||||
// xs, we dont want in .col-slim block
|
||||
> * {
|
||||
flex: 0 0 auto;
|
||||
width: calc(100% / 2);
|
||||
}
|
||||
|
||||
@each $width, $n_cols in $paperless-card-breakpoints {
|
||||
@media(min-width: $width) {
|
||||
> * {
|
||||
flex: 0 0 auto;
|
||||
width: calc(100% / $n-cols);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .col-slim .row-cols-paperless-cards {
|
||||
@each $width, $n_cols in $paperless-card-breakpoints {
|
||||
@media(min-width: $width) {
|
||||
> * {
|
||||
flex: 0 0 auto;
|
||||
width: calc(100% / ($n-cols + 1)) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .document-card-check {
|
||||
display: none !important; // override for dashboard
|
||||
}
|
||||
|
@@ -11,7 +11,13 @@ import { RouterTestingModule } from '@angular/router/testing'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { of, Subject } from 'rxjs'
|
||||
import { routes } from 'src/app/app-routing.module'
|
||||
import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type'
|
||||
import {
|
||||
FILTER_CORRESPONDENT,
|
||||
FILTER_DOCUMENT_TYPE,
|
||||
FILTER_FULLTEXT_MORELIKE,
|
||||
FILTER_HAS_TAGS_ALL,
|
||||
FILTER_STORAGE_PATH,
|
||||
} from 'src/app/data/filter-rule-type'
|
||||
import { SavedView } from 'src/app/data/saved-view'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
@@ -31,6 +37,10 @@ import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop'
|
||||
import { PreviewPopupComponent } from 'src/app/components/common/preview-popup/preview-popup.component'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { CustomFieldDataType } from 'src/app/data/custom-field'
|
||||
import { CustomFieldDisplayComponent } from 'src/app/components/common/custom-field-display/custom-field-display.component'
|
||||
import { DisplayMode, DisplayField } from 'src/app/data/document'
|
||||
|
||||
const savedView: SavedView = {
|
||||
id: 1,
|
||||
@@ -45,17 +55,53 @@ const savedView: SavedView = {
|
||||
value: '1,2',
|
||||
},
|
||||
],
|
||||
page_size: 20,
|
||||
display_mode: DisplayMode.TABLE,
|
||||
display_fields: [
|
||||
DisplayField.CREATED,
|
||||
DisplayField.TITLE,
|
||||
DisplayField.TAGS,
|
||||
DisplayField.CORRESPONDENT,
|
||||
DisplayField.DOCUMENT_TYPE,
|
||||
DisplayField.STORAGE_PATH,
|
||||
`${DisplayField.CUSTOM_FIELD}11` as any,
|
||||
`${DisplayField.CUSTOM_FIELD}15` as any,
|
||||
],
|
||||
}
|
||||
|
||||
const documentResults = [
|
||||
{
|
||||
id: 2,
|
||||
title: 'doc2',
|
||||
custom_fields: [
|
||||
{ id: 1, field: 11, created: new Date(), value: 'custom', document: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'doc3',
|
||||
correspondent: 0,
|
||||
custom_fields: [],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'doc4',
|
||||
custom_fields: [
|
||||
{ id: 32, field: 3, created: new Date(), value: 'EUR123', document: 4 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: 'doc5',
|
||||
custom_fields: [
|
||||
{
|
||||
id: 22,
|
||||
field: 15,
|
||||
created: new Date(),
|
||||
value: [123, 456, 789],
|
||||
document: 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -77,6 +123,7 @@ describe('SavedViewWidgetComponent', () => {
|
||||
DocumentTitlePipe,
|
||||
SafeUrlPipe,
|
||||
PreviewPopupComponent,
|
||||
CustomFieldDisplayComponent,
|
||||
],
|
||||
providers: [
|
||||
PermissionsGuard,
|
||||
@@ -89,6 +136,33 @@ describe('SavedViewWidgetComponent', () => {
|
||||
},
|
||||
CustomDatePipe,
|
||||
DatePipe,
|
||||
{
|
||||
provide: CustomFieldsService,
|
||||
useValue: {
|
||||
listAll: () =>
|
||||
of({
|
||||
all: [3, 11, 15],
|
||||
count: 3,
|
||||
results: [
|
||||
{
|
||||
id: 3,
|
||||
name: 'Custom field 3',
|
||||
data_type: CustomFieldDataType.Monetary,
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: 'Custom Field 11',
|
||||
data_type: CustomFieldDataType.String,
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: 'Custom Field 15',
|
||||
data_type: CustomFieldDataType.DocumentLink,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
@@ -170,7 +244,7 @@ describe('SavedViewWidgetComponent', () => {
|
||||
component.ngOnInit()
|
||||
expect(listAllSpy).toHaveBeenCalledWith(
|
||||
1,
|
||||
10,
|
||||
20,
|
||||
savedView.sort_field,
|
||||
savedView.sort_reverse,
|
||||
savedView.filter_rules,
|
||||
@@ -204,11 +278,78 @@ describe('SavedViewWidgetComponent', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should navigate to document', () => {
|
||||
const routerSpy = jest.spyOn(router, 'navigate')
|
||||
component.openDocumentDetail(documentResults[0])
|
||||
expect(routerSpy).toHaveBeenCalledWith(['documents', documentResults[0].id])
|
||||
})
|
||||
|
||||
it('should navigate via quickfilter on click tag', () => {
|
||||
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
||||
component.clickTag({ id: 11, name: 'Tag11' }, new MouseEvent('click'))
|
||||
component.clickTag(11, new MouseEvent('click'))
|
||||
expect(qfSpy).toHaveBeenCalledWith([
|
||||
{ rule_type: FILTER_HAS_TAGS_ALL, value: '11' },
|
||||
])
|
||||
component.clickTag(11) // coverage
|
||||
})
|
||||
|
||||
it('should navigate via quickfilter on click correspondent', () => {
|
||||
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
||||
component.clickCorrespondent(11, new MouseEvent('click'))
|
||||
expect(qfSpy).toHaveBeenCalledWith([
|
||||
{ rule_type: FILTER_CORRESPONDENT, value: '11' },
|
||||
])
|
||||
component.clickCorrespondent(11) // coverage
|
||||
})
|
||||
|
||||
it('should navigate via quickfilter on click doc type', () => {
|
||||
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
||||
component.clickDocType(11, new MouseEvent('click'))
|
||||
expect(qfSpy).toHaveBeenCalledWith([
|
||||
{ rule_type: FILTER_DOCUMENT_TYPE, value: '11' },
|
||||
])
|
||||
component.clickDocType(11) // coverage
|
||||
})
|
||||
|
||||
it('should navigate via quickfilter on click storage path', () => {
|
||||
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
||||
component.clickStoragePath(11, new MouseEvent('click'))
|
||||
expect(qfSpy).toHaveBeenCalledWith([
|
||||
{ rule_type: FILTER_STORAGE_PATH, value: '11' },
|
||||
])
|
||||
component.clickStoragePath(11) // coverage
|
||||
})
|
||||
|
||||
it('should navigate via quickfilter on click more like', () => {
|
||||
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
||||
component.clickMoreLike(11)
|
||||
expect(qfSpy).toHaveBeenCalledWith([
|
||||
{ rule_type: FILTER_FULLTEXT_MORELIKE, value: '11' },
|
||||
])
|
||||
})
|
||||
|
||||
it('should get correct column title', () => {
|
||||
expect(component.getColumnTitle(DisplayField.TITLE)).toEqual('Title')
|
||||
expect(component.getColumnTitle(DisplayField.CREATED)).toEqual('Created')
|
||||
expect(component.getColumnTitle(DisplayField.ADDED)).toEqual('Added')
|
||||
expect(component.getColumnTitle(DisplayField.TAGS)).toEqual('Tags')
|
||||
expect(component.getColumnTitle(DisplayField.CORRESPONDENT)).toEqual(
|
||||
'Correspondent'
|
||||
)
|
||||
expect(component.getColumnTitle(DisplayField.DOCUMENT_TYPE)).toEqual(
|
||||
'Document type'
|
||||
)
|
||||
expect(component.getColumnTitle(DisplayField.STORAGE_PATH)).toEqual(
|
||||
'Storage path'
|
||||
)
|
||||
})
|
||||
|
||||
it('should get correct column title for custom field', () => {
|
||||
expect(
|
||||
component.getColumnTitle((DisplayField.CUSTOM_FIELD + 11) as any)
|
||||
).toEqual('Custom Field 11')
|
||||
expect(
|
||||
component.getColumnTitle((DisplayField.CUSTOM_FIELD + 15) as any)
|
||||
).toEqual('Custom Field 15')
|
||||
})
|
||||
})
|
||||
|
@@ -6,23 +6,38 @@ import {
|
||||
QueryList,
|
||||
ViewChildren,
|
||||
} from '@angular/core'
|
||||
import { Params, Router } from '@angular/router'
|
||||
import { Router } from '@angular/router'
|
||||
import { Subject, takeUntil } from 'rxjs'
|
||||
import { Document } from 'src/app/data/document'
|
||||
import {
|
||||
DEFAULT_DASHBOARD_DISPLAY_FIELDS,
|
||||
DEFAULT_DASHBOARD_VIEW_PAGE_SIZE,
|
||||
DEFAULT_DISPLAY_FIELDS,
|
||||
DisplayField,
|
||||
DisplayMode,
|
||||
Document,
|
||||
} from 'src/app/data/document'
|
||||
import { SavedView } from 'src/app/data/saved-view'
|
||||
import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { Tag } from 'src/app/data/tag'
|
||||
import {
|
||||
FILTER_CORRESPONDENT,
|
||||
FILTER_DOCUMENT_TYPE,
|
||||
FILTER_FULLTEXT_MORELIKE,
|
||||
FILTER_HAS_TAGS_ALL,
|
||||
FILTER_STORAGE_PATH,
|
||||
} from 'src/app/data/filter-rule-type'
|
||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { ComponentWithPermissions } from 'src/app/components/with-permissions/with-permissions.component'
|
||||
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { queryParamsFromFilterRules } from 'src/app/utils/query-params'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import {
|
||||
PermissionAction,
|
||||
PermissionType,
|
||||
PermissionsService,
|
||||
} from 'src/app/services/permissions.service'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-saved-view-widget',
|
||||
@@ -33,8 +48,14 @@ export class SavedViewWidgetComponent
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
public DisplayMode = DisplayMode
|
||||
public DisplayField = DisplayField
|
||||
public CustomFieldDataType = CustomFieldDataType
|
||||
|
||||
loading: boolean = true
|
||||
|
||||
private customFields: CustomField[] = []
|
||||
|
||||
constructor(
|
||||
private documentService: DocumentService,
|
||||
private router: Router,
|
||||
@@ -42,7 +63,9 @@ export class SavedViewWidgetComponent
|
||||
private consumerStatusService: ConsumerStatusService,
|
||||
public openDocumentsService: OpenDocumentsService,
|
||||
public documentListViewService: DocumentListViewService,
|
||||
public permissionsService: PermissionsService
|
||||
public permissionsService: PermissionsService,
|
||||
private settingsService: SettingsService,
|
||||
private customFieldService: CustomFieldsService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@@ -60,14 +83,44 @@ export class SavedViewWidgetComponent
|
||||
mouseOnPreview = false
|
||||
popoverHidden = true
|
||||
|
||||
displayMode: DisplayMode
|
||||
|
||||
displayFields: DisplayField[] = DEFAULT_DASHBOARD_DISPLAY_FIELDS
|
||||
|
||||
ngOnInit(): void {
|
||||
this.reload()
|
||||
this.displayMode = this.savedView.display_mode ?? DisplayMode.TABLE
|
||||
this.consumerStatusService
|
||||
.onDocumentConsumptionFinished()
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
this.reload()
|
||||
})
|
||||
|
||||
if (
|
||||
this.permissionsService.currentUserCan(
|
||||
PermissionAction.View,
|
||||
PermissionType.CustomField
|
||||
)
|
||||
) {
|
||||
this.customFieldService
|
||||
.listAll()
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((customFields) => {
|
||||
this.customFields = customFields.results
|
||||
})
|
||||
}
|
||||
|
||||
if (this.savedView.display_fields) {
|
||||
this.displayFields = this.savedView.display_fields
|
||||
}
|
||||
|
||||
// filter by perms etc
|
||||
this.displayFields = this.displayFields.filter(
|
||||
(field) =>
|
||||
this.settingsService.allDisplayFields.find((f) => f.id === field) !==
|
||||
undefined
|
||||
)
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@@ -80,7 +133,7 @@ export class SavedViewWidgetComponent
|
||||
this.documentService
|
||||
.listFiltered(
|
||||
1,
|
||||
10,
|
||||
this.savedView.page_size ?? DEFAULT_DASHBOARD_VIEW_PAGE_SIZE,
|
||||
this.savedView.sort_field,
|
||||
this.savedView.sort_reverse,
|
||||
this.savedView.filter_rules,
|
||||
@@ -103,15 +156,52 @@ export class SavedViewWidgetComponent
|
||||
}
|
||||
}
|
||||
|
||||
clickTag(tag: Tag, event: MouseEvent) {
|
||||
event.preventDefault()
|
||||
event.stopImmediatePropagation()
|
||||
clickTag(tagID: number, event: MouseEvent = null) {
|
||||
event?.preventDefault()
|
||||
event?.stopImmediatePropagation()
|
||||
|
||||
this.list.quickFilter([
|
||||
{ rule_type: FILTER_HAS_TAGS_ALL, value: tag.id.toString() },
|
||||
{ rule_type: FILTER_HAS_TAGS_ALL, value: tagID.toString() },
|
||||
])
|
||||
}
|
||||
|
||||
clickCorrespondent(correspondentId: number, event: MouseEvent = null) {
|
||||
event?.preventDefault()
|
||||
event?.stopImmediatePropagation()
|
||||
|
||||
this.list.quickFilter([
|
||||
{ rule_type: FILTER_CORRESPONDENT, value: correspondentId.toString() },
|
||||
])
|
||||
}
|
||||
|
||||
clickDocType(docTypeId: number, event: MouseEvent = null) {
|
||||
event?.preventDefault()
|
||||
event?.stopImmediatePropagation()
|
||||
|
||||
this.list.quickFilter([
|
||||
{ rule_type: FILTER_DOCUMENT_TYPE, value: docTypeId.toString() },
|
||||
])
|
||||
}
|
||||
|
||||
clickStoragePath(storagePathId: number, event: MouseEvent = null) {
|
||||
event?.preventDefault()
|
||||
event?.stopImmediatePropagation()
|
||||
|
||||
this.list.quickFilter([
|
||||
{ rule_type: FILTER_STORAGE_PATH, value: storagePathId.toString() },
|
||||
])
|
||||
}
|
||||
|
||||
clickMoreLike(documentID: number) {
|
||||
this.list.quickFilter([
|
||||
{ rule_type: FILTER_FULLTEXT_MORELIKE, value: documentID.toString() },
|
||||
])
|
||||
}
|
||||
|
||||
openDocumentDetail(document: Document) {
|
||||
this.router.navigate(['documents', document.id])
|
||||
}
|
||||
|
||||
getPreviewUrl(document: Document): string {
|
||||
return this.documentService.getPreviewUrl(document.id)
|
||||
}
|
||||
@@ -161,14 +251,11 @@ export class SavedViewWidgetComponent
|
||||
}, 300)
|
||||
}
|
||||
|
||||
getCorrespondentQueryParams(correspondentId: number): Params {
|
||||
return correspondentId !== undefined
|
||||
? queryParamsFromFilterRules([
|
||||
{
|
||||
rule_type: FILTER_CORRESPONDENT,
|
||||
value: correspondentId.toString(),
|
||||
},
|
||||
])
|
||||
: null
|
||||
public getColumnTitle(field: DisplayField): string {
|
||||
if (field.startsWith(DisplayField.CUSTOM_FIELD)) {
|
||||
const id = field.split('_')[2]
|
||||
return this.customFields.find((f) => f.id === parseInt(id))?.name
|
||||
}
|
||||
return DEFAULT_DISPLAY_FIELDS.find((f) => f.id === field)?.name
|
||||
}
|
||||
}
|
||||
|
@@ -149,7 +149,7 @@ describe('UploadFileWidgetComponent', () => {
|
||||
expect(dismissSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should allow dismissing all alerts', fakeAsync(() => {
|
||||
it('should allow dismissing completed alerts', fakeAsync(() => {
|
||||
mockConsumerStatuses(consumerStatusService)
|
||||
component.alertsExpanded = true
|
||||
fixture.detectChanges()
|
||||
@@ -160,7 +160,7 @@ describe('UploadFileWidgetComponent', () => {
|
||||
component.dismissCompleted()
|
||||
tick(1000)
|
||||
fixture.detectChanges()
|
||||
expect(dismissSpy).toHaveBeenCalledTimes(10)
|
||||
expect(dismissSpy).toHaveBeenCalledTimes(4)
|
||||
}))
|
||||
})
|
||||
|
||||
|
@@ -115,12 +115,9 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions {
|
||||
}
|
||||
|
||||
dismissCompleted() {
|
||||
this.alerts.forEach((a) => a.close())
|
||||
if (this.alertsExpanded) {
|
||||
this.getStatusCompleted().forEach((status) =>
|
||||
this.consumerStatusService.dismiss(status)
|
||||
)
|
||||
}
|
||||
this.getStatusCompleted().forEach((status) =>
|
||||
this.consumerStatusService.dismiss(status)
|
||||
)
|
||||
}
|
||||
|
||||
public onFileSelected(event: Event) {
|
||||
|
@@ -13,11 +13,11 @@
|
||||
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||
<div class="visually-hidden" i18n>Loading...</div>
|
||||
}
|
||||
<ng-content select ="[header-buttons]"></ng-content>
|
||||
<ng-content select="[header-buttons]"></ng-content>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="card-body text-dark">
|
||||
<ng-content select ="[content]"></ng-content>
|
||||
<ng-content select="[content]"></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -57,7 +57,7 @@
|
||||
<i-bs width="1em" height="1em" name="scissors"></i-bs> <span i18n>Split</span>
|
||||
</button>
|
||||
|
||||
<button ngbDropdownItem (click)="rotateDocument()" [disabled]="!userIsOwner || contentRenderType !== ContentRenderType.PDF">
|
||||
<button ngbDropdownItem (click)="rotateDocument()" [disabled]="!userIsOwner || metadata?.original_mime_type !== 'application/pdf'">
|
||||
<i-bs name="arrow-clockwise"></i-bs> <ng-container i18n>Rotate</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
@@ -112,7 +112,7 @@
|
||||
<pngx-input-select [items]="storagePaths" i18n-title title="Storage path" formControlName="storage_path" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)"
|
||||
(createNew)="createStoragePath($event)" [suggestions]="suggestions?.storage_paths" i18n-placeholder placeholder="Default" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }"></pngx-input-select>
|
||||
<pngx-input-tags formControlName="tags" [suggestions]="suggestions?.tags" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"></pngx-input-tags>
|
||||
@for (fieldInstance of document?.custom_fields; track fieldInstance; let i = $index) {
|
||||
@for (fieldInstance of document?.custom_fields; track fieldInstance.field; let i = $index) {
|
||||
<div [formGroup]="customFieldFormFields.controls[i]">
|
||||
@switch (getCustomFieldFromInstance(fieldInstance)?.data_type) {
|
||||
@case (CustomFieldDataType.String) {
|
||||
@@ -285,6 +285,17 @@
|
||||
</li>
|
||||
}
|
||||
|
||||
@if (historyEnabled) {
|
||||
<li [ngbNavItem]="DocumentDetailNavIDs.History">
|
||||
<a ngbNavLink i18n>History</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="mb-3">
|
||||
<pngx-document-history [documentId]="documentId"></pngx-document-history>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
}
|
||||
|
||||
@if (showPermissions) {
|
||||
<li [ngbNavItem]="DocumentDetailNavIDs.Permissions">
|
||||
<a ngbNavLink i18n>Permissions</a>
|
||||
|
@@ -17,7 +17,7 @@
|
||||
--page-margin: 10px auto;
|
||||
}
|
||||
|
||||
::ng-deep .ng-select-taggable {
|
||||
::ng-deep form .ng-select-taggable {
|
||||
max-width: calc(100% - 90px); // fudge factor for (2x) ng-select button width
|
||||
}
|
||||
|
||||
|
@@ -77,6 +77,7 @@ enum DocumentDetailNavIDs {
|
||||
Preview = 4,
|
||||
Notes = 5,
|
||||
Permissions = 6,
|
||||
History = 7,
|
||||
}
|
||||
|
||||
enum ContentRenderType {
|
||||
@@ -902,6 +903,17 @@ export class DocumentDetailComponent
|
||||
)
|
||||
}
|
||||
|
||||
get historyEnabled(): boolean {
|
||||
return (
|
||||
this.settings.get(SETTINGS_KEYS.AUDITLOG_ENABLED) &&
|
||||
this.userIsOwner &&
|
||||
this.permissionsService.currentUserCan(
|
||||
PermissionAction.View,
|
||||
PermissionType.History
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
notesUpdated(notes: DocumentNote[]) {
|
||||
this.document.notes = notes
|
||||
this.openDocumentService.refreshDocument(this.documentId)
|
||||
|
@@ -0,0 +1,59 @@
|
||||
@if (loading) {
|
||||
<div class="d-flex">
|
||||
<div class="spinner-border spinner-border-sm fw-normal" role="status"></div>
|
||||
</div>
|
||||
} @else {
|
||||
<ul class="list-group">
|
||||
@if (entries.length === 0) {
|
||||
<li class="list-group-item">
|
||||
<div class="d-flex justify-content-center">
|
||||
<span class="fst-italic" i18n>No entries found.</span>
|
||||
</div>
|
||||
</li>
|
||||
} @else {
|
||||
@for (entry of entries; track entry.id) {
|
||||
<li class="list-group-item">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<ng-template #timestamp>
|
||||
<div class="text-light">
|
||||
{{ entry.timestamp | customDate:'longDate' }} {{ entry.timestamp | date:'shortTime' }}
|
||||
</div>
|
||||
</ng-template>
|
||||
<span class="text-muted" [ngbTooltip]="timestamp">{{ entry.timestamp | customDate:'relative' }}</span>
|
||||
@if (entry.actor) {
|
||||
<span class="ms-3 fst-italic">{{ entry.actor.username }}</span>
|
||||
} @else {
|
||||
<span class="ms-3 fst-italic">System</span>
|
||||
}
|
||||
<span class="badge bg-secondary ms-auto" [class.bg-primary]="entry.action === AuditLogAction.Create">{{ entry.action | titlecase }}</span>
|
||||
</div>
|
||||
@if (entry.action === AuditLogAction.Update) {
|
||||
<ul class="mt-2">
|
||||
@for (change of entry.changes | keyvalue; track change.key) {
|
||||
@if (change.value["type"] === 'm2m') {
|
||||
<li>
|
||||
<span class="fst-italic" i18n>{{ change.value["operation"] | titlecase }}</span>
|
||||
<span class="text-light">{{ change.key | titlecase }}</span>:
|
||||
<code class="text-primary">{{ change.value["objects"].join(', ') }}</code>
|
||||
</li>
|
||||
}
|
||||
@else if (change.value["type"] === 'custom_field') {
|
||||
<li>
|
||||
<span class="text-light">{{ change.value["field"] }}</span>:
|
||||
<code class="text-primary">{{ change.value["value"] }}</code>
|
||||
</li>
|
||||
}
|
||||
@else {
|
||||
<li>
|
||||
<span class="text-light">{{ change.key | titlecase }}</span>:
|
||||
<code class="text-primary">{{ change.value[1] }}</code>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
}
|
@@ -0,0 +1,58 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
|
||||
import { DocumentHistoryComponent } from './document-history.component'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { of } from 'rxjs'
|
||||
import { AuditLogAction } from 'src/app/data/auditlog-entry'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { DatePipe } from '@angular/common'
|
||||
import { NgbCollapseModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
|
||||
describe('DocumentHistoryComponent', () => {
|
||||
let component: DocumentHistoryComponent
|
||||
let fixture: ComponentFixture<DocumentHistoryComponent>
|
||||
let documentService: DocumentService
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [DocumentHistoryComponent, CustomDatePipe],
|
||||
providers: [DatePipe],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
NgbCollapseModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
NgbTooltipModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(DocumentHistoryComponent)
|
||||
documentService = TestBed.inject(DocumentService)
|
||||
component = fixture.componentInstance
|
||||
})
|
||||
|
||||
it('should get audit log entries on init', () => {
|
||||
const getHistorySpy = jest.spyOn(documentService, 'getHistory')
|
||||
getHistorySpy.mockReturnValue(
|
||||
of([
|
||||
{
|
||||
id: 1,
|
||||
actor: {
|
||||
id: 1,
|
||||
username: 'user1',
|
||||
},
|
||||
action: AuditLogAction.Create,
|
||||
timestamp: '2021-01-01T00:00:00Z',
|
||||
remote_addr: '1.2.3.4',
|
||||
changes: {
|
||||
title: ['old title', 'new title'],
|
||||
},
|
||||
},
|
||||
])
|
||||
)
|
||||
component.documentId = 1
|
||||
fixture.detectChanges()
|
||||
expect(getHistorySpy).toHaveBeenCalledWith(1)
|
||||
})
|
||||
})
|
@@ -0,0 +1,36 @@
|
||||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { AuditLogAction, AuditLogEntry } from 'src/app/data/auditlog-entry'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-document-history',
|
||||
templateUrl: './document-history.component.html',
|
||||
styleUrl: './document-history.component.scss',
|
||||
})
|
||||
export class DocumentHistoryComponent implements OnInit {
|
||||
public AuditLogAction = AuditLogAction
|
||||
|
||||
private _documentId: number
|
||||
@Input()
|
||||
set documentId(id: number) {
|
||||
this._documentId = id
|
||||
this.ngOnInit()
|
||||
}
|
||||
|
||||
public loading: boolean = true
|
||||
public entries: AuditLogEntry[] = []
|
||||
|
||||
constructor(private documentService: DocumentService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this._documentId) {
|
||||
this.loading = true
|
||||
this.documentService
|
||||
.getHistory(this._documentId)
|
||||
.subscribe((auditLogEntries) => {
|
||||
this.entries = auditLogEntries
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@@ -74,6 +74,20 @@
|
||||
(apply)="setStoragePaths($event)">
|
||||
</pngx-filterable-dropdown>
|
||||
}
|
||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.CustomField)) {
|
||||
<pngx-filterable-dropdown title="Custom fields" icon="ui-radios" i18n-title
|
||||
filterPlaceholder="Filter custom fields" i18n-filterPlaceholder
|
||||
[items]="customFields"
|
||||
[disabled]="!userCanEditAll"
|
||||
[editing]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
[createRef]="createCustomField.bind(this)"
|
||||
(opened)="openCustomFieldsDropdown()"
|
||||
[(selectionModel)]="customFieldsSelectionModel"
|
||||
[documentCounts]="customFieldDocumentCounts"
|
||||
(apply)="setCustomFields($event)">
|
||||
</pngx-filterable-dropdown>
|
||||
}
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2 ms-auto">
|
||||
<div class="btn-toolbar">
|
||||
|
@@ -55,6 +55,9 @@ import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage
|
||||
import { IsNumberPipe } from 'src/app/pipes/is-number.pipe'
|
||||
import { RotateConfirmDialogComponent } from '../../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
|
||||
import { MergeConfirmDialogComponent } from '../../common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
||||
|
||||
const selectionData: SelectionData = {
|
||||
selected_tags: [
|
||||
@@ -68,6 +71,10 @@ const selectionData: SelectionData = {
|
||||
{ id: 66, document_count: 3 },
|
||||
{ id: 55, document_count: 0 },
|
||||
],
|
||||
selected_custom_fields: [
|
||||
{ id: 77, document_count: 3 },
|
||||
{ id: 88, document_count: 0 },
|
||||
],
|
||||
}
|
||||
|
||||
describe('BulkEditorComponent', () => {
|
||||
@@ -82,6 +89,7 @@ describe('BulkEditorComponent', () => {
|
||||
let correspondentsService: CorrespondentService
|
||||
let documentTypeService: DocumentTypeService
|
||||
let storagePathService: StoragePathService
|
||||
let customFieldsService: CustomFieldsService
|
||||
let httpTestingController: HttpTestingController
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -148,6 +156,18 @@ describe('BulkEditorComponent', () => {
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CustomFieldsService,
|
||||
useValue: {
|
||||
listAll: () =>
|
||||
of({
|
||||
results: [
|
||||
{ id: 77, name: 'customfield1' },
|
||||
{ id: 88, name: 'customfield2' },
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
FilterPipe,
|
||||
SettingsService,
|
||||
{
|
||||
@@ -189,6 +209,7 @@ describe('BulkEditorComponent', () => {
|
||||
correspondentsService = TestBed.inject(CorrespondentService)
|
||||
documentTypeService = TestBed.inject(DocumentTypeService)
|
||||
storagePathService = TestBed.inject(StoragePathService)
|
||||
customFieldsService = TestBed.inject(CustomFieldsService)
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
|
||||
fixture = TestBed.createComponent(BulkEditorComponent)
|
||||
@@ -262,6 +283,22 @@ describe('BulkEditorComponent', () => {
|
||||
expect(component.storagePathsSelectionModel.selectionSize()).toEqual(1)
|
||||
})
|
||||
|
||||
it('should apply selection data to custom fields menu', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
fixture.detectChanges()
|
||||
expect(
|
||||
component.customFieldsSelectionModel.getSelectedItems()
|
||||
).toHaveLength(0)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 5, 7]))
|
||||
jest
|
||||
.spyOn(documentService, 'getSelectionData')
|
||||
.mockReturnValue(of(selectionData))
|
||||
component.openCustomFieldsDropdown()
|
||||
expect(component.customFieldsSelectionModel.selectionSize()).toEqual(1)
|
||||
})
|
||||
|
||||
it('should execute modify tags bulk operation', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
@@ -679,6 +716,122 @@ describe('BulkEditorComponent', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should execute modify custom fields bulk operation', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 4]))
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||
.mockReturnValue(true)
|
||||
component.showConfirmationDialogs = false
|
||||
fixture.detectChanges()
|
||||
component.setCustomFields({
|
||||
itemsToAdd: [{ id: 101 }],
|
||||
itemsToRemove: [{ id: 102 }],
|
||||
})
|
||||
let req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||
)
|
||||
req.flush(true)
|
||||
expect(req.request.body).toEqual({
|
||||
documents: [3, 4],
|
||||
method: 'modify_custom_fields',
|
||||
parameters: { add_custom_fields: [101], remove_custom_fields: [102] },
|
||||
})
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||
) // list reload
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||
) // listAllFilteredIds
|
||||
})
|
||||
|
||||
it('should execute modify custom fields bulk operation with confirmation dialog if enabled', () => {
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((m) => (modal = m[0]))
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 4]))
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||
.mockReturnValue(true)
|
||||
component.showConfirmationDialogs = true
|
||||
fixture.detectChanges()
|
||||
component.setCustomFields({
|
||||
itemsToAdd: [{ id: 101 }],
|
||||
itemsToRemove: [{ id: 102 }],
|
||||
})
|
||||
expect(modal).not.toBeUndefined()
|
||||
modal.componentInstance.confirm()
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}documents/bulk_edit/`)
|
||||
.flush(true)
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||
) // list reload
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||
) // listAllFilteredIds
|
||||
|
||||
// coverage for modal messages
|
||||
component.setCustomFields({
|
||||
itemsToAdd: [{ id: 101 }],
|
||||
itemsToRemove: [],
|
||||
})
|
||||
component.setCustomFields({
|
||||
itemsToAdd: [{ id: 101 }, { id: 102 }],
|
||||
itemsToRemove: [],
|
||||
})
|
||||
component.setCustomFields({
|
||||
itemsToAdd: [],
|
||||
itemsToRemove: [{ id: 101 }, { id: 102 }],
|
||||
})
|
||||
component.setCustomFields({
|
||||
itemsToAdd: [{ id: 100 }],
|
||||
itemsToRemove: [{ id: 101 }, { id: 102 }],
|
||||
})
|
||||
})
|
||||
|
||||
it('should set modal dialog text accordingly for custom fields edit confirmation', () => {
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 4]))
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||
.mockReturnValue(true)
|
||||
component.showConfirmationDialogs = true
|
||||
fixture.detectChanges()
|
||||
component.setCustomFields({
|
||||
itemsToAdd: [],
|
||||
itemsToRemove: [{ id: 101, name: 'CustomField 101' }],
|
||||
})
|
||||
expect(modal.componentInstance.message).toEqual(
|
||||
'This operation will remove the custom field "CustomField 101" from 2 selected document(s).'
|
||||
)
|
||||
modal.close()
|
||||
component.setCustomFields({
|
||||
itemsToAdd: [{ id: 101, name: 'CustomField 101' }],
|
||||
itemsToRemove: [],
|
||||
})
|
||||
expect(modal.componentInstance.message).toEqual(
|
||||
'This operation will assign the custom field "CustomField 101" to 2 selected document(s).'
|
||||
)
|
||||
})
|
||||
|
||||
it('should only execute bulk operations when changes are detected', () => {
|
||||
component.setTags({
|
||||
itemsToAdd: [],
|
||||
@@ -696,6 +849,10 @@ describe('BulkEditorComponent', () => {
|
||||
itemsToAdd: [],
|
||||
itemsToRemove: [],
|
||||
})
|
||||
component.setCustomFields({
|
||||
itemsToAdd: [],
|
||||
itemsToRemove: [],
|
||||
})
|
||||
httpTestingController.expectNone(
|
||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||
)
|
||||
@@ -866,9 +1023,13 @@ describe('BulkEditorComponent', () => {
|
||||
jest
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||
jest
|
||||
.spyOn(documentService, 'getCachedMany')
|
||||
.mockReturnValue(of([{ id: 3 }, { id: 4 }]))
|
||||
jest.spyOn(documentService, 'getFew').mockReturnValue(
|
||||
of({
|
||||
all: [3, 4],
|
||||
count: 2,
|
||||
results: [{ id: 3 }, { id: 4 }],
|
||||
})
|
||||
)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 4]))
|
||||
@@ -1175,4 +1336,56 @@ describe('BulkEditorComponent', () => {
|
||||
)
|
||||
expect(component.storagePaths).toEqual(storagePaths.results)
|
||||
})
|
||||
|
||||
it('should support create new custom field', () => {
|
||||
const name = 'New Custom Field'
|
||||
const newCustomField = { id: 101, name: 'New Custom Field' }
|
||||
const customFields: Results<CustomField> = {
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Custom Field 1',
|
||||
data_type: CustomFieldDataType.String,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Custom Field 2',
|
||||
data_type: CustomFieldDataType.String,
|
||||
},
|
||||
],
|
||||
count: 2,
|
||||
all: [1, 2],
|
||||
}
|
||||
|
||||
const modalInstance = {
|
||||
componentInstance: {
|
||||
dialogMode: EditDialogMode.CREATE,
|
||||
object: { name },
|
||||
succeeded: of(newCustomField),
|
||||
},
|
||||
}
|
||||
const customFieldsListAllSpy = jest.spyOn(customFieldsService, 'listAll')
|
||||
customFieldsListAllSpy.mockReturnValue(of(customFields))
|
||||
|
||||
const customFieldsSelectionModelToggleSpy = jest.spyOn(
|
||||
component.customFieldsSelectionModel,
|
||||
'toggle'
|
||||
)
|
||||
|
||||
const modalServiceOpenSpy = jest.spyOn(modalService, 'open')
|
||||
modalServiceOpenSpy.mockReturnValue(modalInstance as any)
|
||||
|
||||
component.createCustomField(name)
|
||||
|
||||
expect(modalServiceOpenSpy).toHaveBeenCalledWith(
|
||||
CustomFieldEditDialogComponent,
|
||||
{ backdrop: 'static' }
|
||||
)
|
||||
expect(customFieldsListAllSpy).toHaveBeenCalled()
|
||||
|
||||
expect(customFieldsSelectionModelToggleSpy).toHaveBeenCalledWith(
|
||||
newCustomField.id
|
||||
)
|
||||
expect(component.customFields).toEqual(customFields.results)
|
||||
})
|
||||
})
|
||||
|
@@ -41,6 +41,9 @@ import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/docume
|
||||
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||
import { RotateConfirmDialogComponent } from '../../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
|
||||
import { MergeConfirmDialogComponent } from '../../common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component'
|
||||
import { CustomField } from 'src/app/data/custom-field'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-bulk-editor',
|
||||
@@ -55,15 +58,18 @@ export class BulkEditorComponent
|
||||
correspondents: Correspondent[]
|
||||
documentTypes: DocumentType[]
|
||||
storagePaths: StoragePath[]
|
||||
customFields: CustomField[]
|
||||
|
||||
tagSelectionModel = new FilterableDropdownSelectionModel()
|
||||
correspondentSelectionModel = new FilterableDropdownSelectionModel()
|
||||
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
|
||||
storagePathsSelectionModel = new FilterableDropdownSelectionModel()
|
||||
customFieldsSelectionModel = new FilterableDropdownSelectionModel()
|
||||
tagDocumentCounts: SelectionDataItem[]
|
||||
correspondentDocumentCounts: SelectionDataItem[]
|
||||
documentTypeDocumentCounts: SelectionDataItem[]
|
||||
storagePathDocumentCounts: SelectionDataItem[]
|
||||
customFieldDocumentCounts: SelectionDataItem[]
|
||||
awaitingDownload: boolean
|
||||
|
||||
unsubscribeNotifier: Subject<any> = new Subject()
|
||||
@@ -85,6 +91,7 @@ export class BulkEditorComponent
|
||||
private settings: SettingsService,
|
||||
private toastService: ToastService,
|
||||
private storagePathService: StoragePathService,
|
||||
private customFieldService: CustomFieldsService,
|
||||
private permissionService: PermissionsService
|
||||
) {
|
||||
super()
|
||||
@@ -166,6 +173,17 @@ export class BulkEditorComponent
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.storagePaths = result.results))
|
||||
}
|
||||
if (
|
||||
this.permissionService.currentUserCan(
|
||||
PermissionAction.View,
|
||||
PermissionType.CustomField
|
||||
)
|
||||
) {
|
||||
this.customFieldService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.customFields = result.results))
|
||||
}
|
||||
|
||||
this.downloadForm
|
||||
.get('downloadFileTypeArchive')
|
||||
@@ -297,6 +315,19 @@ export class BulkEditorComponent
|
||||
})
|
||||
}
|
||||
|
||||
openCustomFieldsDropdown() {
|
||||
this.documentService
|
||||
.getSelectionData(Array.from(this.list.selected))
|
||||
.pipe(first())
|
||||
.subscribe((s) => {
|
||||
this.customFieldDocumentCounts = s.selected_custom_fields
|
||||
this.applySelectionData(
|
||||
s.selected_custom_fields,
|
||||
this.customFieldsSelectionModel
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private _localizeList(items: MatchingModel[]) {
|
||||
if (items.length == 0) {
|
||||
return ''
|
||||
@@ -495,6 +526,74 @@ export class BulkEditorComponent
|
||||
}
|
||||
}
|
||||
|
||||
setCustomFields(changedCustomFields: ChangedItems) {
|
||||
if (
|
||||
changedCustomFields.itemsToAdd.length == 0 &&
|
||||
changedCustomFields.itemsToRemove.length == 0
|
||||
)
|
||||
return
|
||||
|
||||
if (this.showConfirmationDialogs) {
|
||||
let modal = this.modalService.open(ConfirmDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.title = $localize`Confirm custom field assignment`
|
||||
if (
|
||||
changedCustomFields.itemsToAdd.length == 1 &&
|
||||
changedCustomFields.itemsToRemove.length == 0
|
||||
) {
|
||||
let customField = changedCustomFields.itemsToAdd[0]
|
||||
modal.componentInstance.message = $localize`This operation will assign the custom field "${customField.name}" to ${this.list.selected.size} selected document(s).`
|
||||
} else if (
|
||||
changedCustomFields.itemsToAdd.length > 1 &&
|
||||
changedCustomFields.itemsToRemove.length == 0
|
||||
) {
|
||||
modal.componentInstance.message = $localize`This operation will assign the custom fields ${this._localizeList(
|
||||
changedCustomFields.itemsToAdd
|
||||
)} to ${this.list.selected.size} selected document(s).`
|
||||
} else if (
|
||||
changedCustomFields.itemsToAdd.length == 0 &&
|
||||
changedCustomFields.itemsToRemove.length == 1
|
||||
) {
|
||||
let customField = changedCustomFields.itemsToRemove[0]
|
||||
modal.componentInstance.message = $localize`This operation will remove the custom field "${customField.name}" from ${this.list.selected.size} selected document(s).`
|
||||
} else if (
|
||||
changedCustomFields.itemsToAdd.length == 0 &&
|
||||
changedCustomFields.itemsToRemove.length > 1
|
||||
) {
|
||||
modal.componentInstance.message = $localize`This operation will remove the custom fields ${this._localizeList(
|
||||
changedCustomFields.itemsToRemove
|
||||
)} from ${this.list.selected.size} selected document(s).`
|
||||
} else {
|
||||
modal.componentInstance.message = $localize`This operation will assign the custom fields ${this._localizeList(
|
||||
changedCustomFields.itemsToAdd
|
||||
)} and remove the custom fields ${this._localizeList(
|
||||
changedCustomFields.itemsToRemove
|
||||
)} on ${this.list.selected.size} selected document(s).`
|
||||
}
|
||||
|
||||
modal.componentInstance.btnClass = 'btn-warning'
|
||||
modal.componentInstance.btnCaption = $localize`Confirm`
|
||||
modal.componentInstance.confirmClicked
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
this.executeBulkOperation(modal, 'modify_custom_fields', {
|
||||
add_custom_fields: changedCustomFields.itemsToAdd.map((f) => f.id),
|
||||
remove_custom_fields: changedCustomFields.itemsToRemove.map(
|
||||
(f) => f.id
|
||||
),
|
||||
})
|
||||
})
|
||||
} else {
|
||||
this.executeBulkOperation(null, 'modify_custom_fields', {
|
||||
add_custom_fields: changedCustomFields.itemsToAdd.map((f) => f.id),
|
||||
remove_custom_fields: changedCustomFields.itemsToRemove.map(
|
||||
(f) => f.id
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
createTag(name: string) {
|
||||
let modal = this.modalService.open(TagEditDialogComponent, {
|
||||
backdrop: 'static',
|
||||
@@ -581,6 +680,27 @@ export class BulkEditorComponent
|
||||
})
|
||||
}
|
||||
|
||||
createCustomField(name: string) {
|
||||
let modal = this.modalService.open(CustomFieldEditDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.dialogMode = EditDialogMode.CREATE
|
||||
modal.componentInstance.object = { name }
|
||||
modal.componentInstance.succeeded
|
||||
.pipe(
|
||||
switchMap((newCustomField) => {
|
||||
return this.customFieldService
|
||||
.listAll()
|
||||
.pipe(map((customFields) => ({ newCustomField, customFields })))
|
||||
})
|
||||
)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(({ newCustomField, customFields }) => {
|
||||
this.customFields = customFields.results
|
||||
this.customFieldsSelectionModel.toggle(newCustomField.id)
|
||||
})
|
||||
}
|
||||
|
||||
applyDelete() {
|
||||
let modal = this.modalService.open(ConfirmDialogComponent, {
|
||||
backdrop: 'static',
|
||||
|
@@ -15,7 +15,7 @@
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title">
|
||||
@if (document.correspondent) {
|
||||
@if (displayFields.includes(DisplayField.CORRESPONDENT) && document.correspondent) {
|
||||
@if (clickCorrespondent.observers.length ) {
|
||||
<a title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name}}</a>
|
||||
} @else {
|
||||
@@ -23,14 +23,18 @@
|
||||
}
|
||||
:
|
||||
}
|
||||
{{document.title | documentTitle}}
|
||||
@for (t of document.tags$ | async; track t) {
|
||||
<pngx-tag [tag]="t" linkTitle="Filter by tag" i18n-linkTitle class="ms-1" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="clickTag.observers.length"></pngx-tag>
|
||||
@if (displayFields.includes(DisplayField.TITLE)) {
|
||||
{{document.title | documentTitle}}
|
||||
}
|
||||
@if (displayFields.includes(DisplayField.TAGS)) {
|
||||
@for (t of document.tags$ | async; track t) {
|
||||
<pngx-tag [tag]="t" linkTitle="Filter by tag" i18n-linkTitle class="ms-1" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="clickTag.observers.length"></pngx-tag>
|
||||
}
|
||||
}
|
||||
</h5>
|
||||
</div>
|
||||
<p class="card-text">
|
||||
@if (document.__search_hit__ && document.__search_hit__.highlights) {
|
||||
@if (document.__search_hit__?.score && document.__search_hit__.highlights) {
|
||||
<span [innerHtml]="document.__search_hit__.highlights"></span>
|
||||
}
|
||||
@for (highlight of searchNoteHighlights; track highlight) {
|
||||
@@ -39,7 +43,7 @@
|
||||
<span [innerHtml]="highlight"></span>
|
||||
</span>
|
||||
}
|
||||
@if (!document.__search_hit__) {
|
||||
@if (!document.__search_hit__?.score) {
|
||||
<span class="result-content">{{contentTrimmed}}</span>
|
||||
}
|
||||
</p>
|
||||
@@ -66,44 +70,53 @@
|
||||
</div>
|
||||
|
||||
<div class="list-group list-group-horizontal border-0 card-info ms-md-auto mt-2 mt-md-0">
|
||||
@if (notesEnabled && document.notes.length) {
|
||||
@if (displayFields.includes(DisplayField.NOTES) && notesEnabled && document.notes.length) {
|
||||
<button routerLink="/documents/{{document.id}}/notes" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2 d-flex align-items-center" title="View notes" i18n-title>
|
||||
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="chat-left-text"></i-bs><small i18n>{{document.notes.length}} Notes</small>
|
||||
</button>
|
||||
}
|
||||
@if (document.document_type) {
|
||||
@if (displayFields.includes(DisplayField.DOCUMENT_TYPE) && document.document_type) {
|
||||
<button type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2 d-flex align-items-center" title="Filter by document type" i18n-title
|
||||
(click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
|
||||
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="file-earmark"></i-bs><small>{{(document.document_type$ | async)?.name}}</small>
|
||||
</button>
|
||||
}
|
||||
@if (document.storage_path) {
|
||||
@if (displayFields.includes(DisplayField.STORAGE_PATH) && document.storage_path) {
|
||||
<button type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2 d-flex align-items-center" title="Filter by storage path" i18n-title
|
||||
(click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
|
||||
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="archive"></i-bs><small>{{(document.storage_path$ | async)?.name}}</small>
|
||||
</button>
|
||||
}
|
||||
@if (document.archive_serial_number | isNumber) {
|
||||
@if (displayFields.includes(DisplayField.ASN) && document.archive_serial_number | isNumber) {
|
||||
<div class="list-group-item me-2 bg-light text-dark p-1 border-0 d-flex align-items-center">
|
||||
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="upc-scan"></i-bs><small>#{{document.archive_serial_number}}</small>
|
||||
</div>
|
||||
}
|
||||
<ng-template #dateTooltip>
|
||||
<div class="d-flex flex-column text-light">
|
||||
<span i18n>Created: {{ document.created | customDate }}</span>
|
||||
<span i18n>Added: {{ document.added | customDate }}</span>
|
||||
<span i18n>Modified: {{ document.modified | customDate }}</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center" [ngbTooltip]="dateTooltip">
|
||||
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="calendar-event"></i-bs><small>{{document.created_date | customDate:'mediumDate'}}</small>
|
||||
</div>
|
||||
@if (document.owner && document.owner !== settingsService.currentUser.id) {
|
||||
@if (displayFields.includes(DisplayField.CREATED) || displayFields.includes(DisplayField.ADDED)) {
|
||||
<ng-template #dateTooltip>
|
||||
<div class="d-flex flex-column text-light">
|
||||
<span i18n>Created: {{ document.created | customDate }}</span>
|
||||
<span i18n>Added: {{ document.added | customDate }}</span>
|
||||
<span i18n>Modified: {{ document.modified | customDate }}</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
@if (displayFields.includes(DisplayField.CREATED)) {
|
||||
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center" [ngbTooltip]="dateTooltip">
|
||||
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="calendar-event"></i-bs><small>{{document.created_date | customDate:'mediumDate'}}</small>
|
||||
</div>
|
||||
}
|
||||
@if (displayFields.includes(DisplayField.ADDED)) {
|
||||
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center" [ngbTooltip]="dateTooltip">
|
||||
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="calendar-event"></i-bs><small>{{document.added | customDate:'mediumDate'}}</small>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (displayFields.includes(DisplayField.OWNER) && document.owner && document.owner !== settingsService.currentUser.id) {
|
||||
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center">
|
||||
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="person-fill-lock"></i-bs><small>{{document.owner | username}}</small>
|
||||
</div>
|
||||
}
|
||||
@if (document.is_shared_by_requester) {
|
||||
@if (displayFields.includes(DisplayField.SHARED) && document.is_shared_by_requester) {
|
||||
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center">
|
||||
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="people-fill"></i-bs><small i18n>Shared</small>
|
||||
</div>
|
||||
@@ -114,6 +127,16 @@
|
||||
<ngb-progressbar [type]="searchScoreClass" [value]="document.__search_hit__.score" class="search-score-bar mx-2 mt-1" [max]="1"></ngb-progressbar>
|
||||
</div>
|
||||
}
|
||||
@for (field of document.custom_fields; track field.id) {
|
||||
@if (displayFields.includes(DisplayField.CUSTOM_FIELD + field.field)) {
|
||||
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center">
|
||||
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="ui-radios"></i-bs>
|
||||
<small>
|
||||
<pngx-custom-field-display [document]="document" [fieldId]="field.field"></pngx-custom-field-display>
|
||||
</small>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -21,6 +21,7 @@ import { DocumentCardLargeComponent } from './document-card-large.component'
|
||||
import { IsNumberPipe } from 'src/app/pipes/is-number.pipe'
|
||||
import { PreviewPopupComponent } from '../../common/preview-popup/preview-popup.component'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { CustomFieldDisplayComponent } from '../../common/custom-field-display/custom-field-display.component'
|
||||
|
||||
const doc = {
|
||||
id: 10,
|
||||
@@ -53,6 +54,7 @@ describe('DocumentCardLargeComponent', () => {
|
||||
SafeUrlPipe,
|
||||
IsNumberPipe,
|
||||
PreviewPopupComponent,
|
||||
CustomFieldDisplayComponent,
|
||||
],
|
||||
providers: [DatePipe],
|
||||
imports: [
|
||||
|
@@ -5,7 +5,11 @@ import {
|
||||
Output,
|
||||
ViewChild,
|
||||
} from '@angular/core'
|
||||
import { Document } from 'src/app/data/document'
|
||||
import {
|
||||
DEFAULT_DISPLAY_FIELDS,
|
||||
DisplayField,
|
||||
Document,
|
||||
} from 'src/app/data/document'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
||||
@@ -18,6 +22,8 @@ import { ComponentWithPermissions } from '../../with-permissions/with-permission
|
||||
styleUrls: ['./document-card-large.component.scss'],
|
||||
})
|
||||
export class DocumentCardLargeComponent extends ComponentWithPermissions {
|
||||
DisplayField = DisplayField
|
||||
|
||||
constructor(
|
||||
private documentService: DocumentService,
|
||||
public settingsService: SettingsService
|
||||
@@ -28,6 +34,9 @@ export class DocumentCardLargeComponent extends ComponentWithPermissions {
|
||||
@Input()
|
||||
selected = false
|
||||
|
||||
@Input()
|
||||
displayFields: string[] = DEFAULT_DISPLAY_FIELDS.map((f) => f.id)
|
||||
|
||||
@Output()
|
||||
toggleSelected = new EventEmitter()
|
||||
|
||||
|
@@ -10,19 +10,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tags d-flex flex-column text-end position-absolute me-1 fs-6">
|
||||
@for (t of getTagsLimited$() | async; track t) {
|
||||
<pngx-tag [tag]="t" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="true" linkTitle="Toggle tag filter" i18n-linkTitle></pngx-tag>
|
||||
}
|
||||
@if (moreTags) {
|
||||
<div>
|
||||
<span class="badge text-dark">+ {{moreTags}}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (displayFields?.includes(DisplayField.TAGS)) {
|
||||
<div class="tags d-flex flex-column text-end position-absolute me-1 fs-6">
|
||||
@for (t of getTagsLimited$() | async; track t) {
|
||||
<pngx-tag [tag]="t" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="true" linkTitle="Toggle tag filter" i18n-linkTitle></pngx-tag>
|
||||
}
|
||||
@if (moreTags) {
|
||||
<div>
|
||||
<span class="badge text-dark">+ {{moreTags}}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (notesEnabled && document.notes.length) {
|
||||
@if (displayFields.includes(DisplayField.NOTES) && notesEnabled && document.notes.length) {
|
||||
<a routerLink="/documents/{{document.id}}/notes" class="document-card-notes py-2 px-1">
|
||||
<span class="badge rounded-pill bg-light border text-primary">
|
||||
<i-bs width="1.2em" height="1.2em" class="ms-1 me-1" name="chat-left-text"></i-bs>
|
||||
@@ -32,59 +34,86 @@
|
||||
|
||||
<div class="card-body bg-light p-2">
|
||||
<p class="card-text">
|
||||
@if (document.correspondent) {
|
||||
@if (displayFields.includes(DisplayField.CORRESPONDENT) && document.correspondent) {
|
||||
<a title="Toggle correspondent filter" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name ?? privateName}}</a>:
|
||||
}
|
||||
{{document.title | documentTitle}}
|
||||
@if (displayFields.includes(DisplayField.TITLE)) {
|
||||
{{document.title | documentTitle}}
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-footer pt-0 pb-2 px-2">
|
||||
<div class="list-group list-group-flush border-0 pt-1 pb-2 card-info">
|
||||
@if (document.document_type) {
|
||||
@if (displayFields.includes(DisplayField.DOCUMENT_TYPE) && document.document_type) {
|
||||
<button type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle document type filter" i18n-title
|
||||
(click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
|
||||
<i-bs width="1em" height="1em" class="me-2 text-muted" name="file-earmark"></i-bs>
|
||||
<small>{{(document.document_type$ | async)?.name ?? privateName}}</small>
|
||||
</button>
|
||||
}
|
||||
@if (document.storage_path) {
|
||||
@if (displayFields.includes(DisplayField.STORAGE_PATH) && document.storage_path) {
|
||||
<button type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle storage path filter" i18n-title
|
||||
(click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
|
||||
<i-bs width="1em" height="1em" class="me-2 text-muted" name="folder"></i-bs>
|
||||
<small>{{(document.storage_path$ | async)?.name ?? privateName}}</small>
|
||||
</button>
|
||||
}
|
||||
<div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between">
|
||||
<ng-template #dateTooltip>
|
||||
<div class="d-flex flex-column text-light">
|
||||
<span i18n>Created: {{ document.created | customDate }}</span>
|
||||
<span i18n>Added: {{ document.added | customDate }}</span>
|
||||
<span i18n>Modified: {{ document.modified | customDate }}</span>
|
||||
@if (displayFields.includes(DisplayField.CREATED)) {
|
||||
<div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between">
|
||||
<ng-template #dateTooltip>
|
||||
<div class="d-flex flex-column text-light">
|
||||
<span i18n>Created: {{ document.created | customDate }}</span>
|
||||
<span i18n>Added: {{ document.added | customDate }}</span>
|
||||
<span i18n>Modified: {{ document.modified | customDate }}</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div class="ps-0 p-1" placement="top" [ngbTooltip]="dateTooltip">
|
||||
<i-bs width="1em" height="1em" class="me-2 text-muted" name="calendar-event"></i-bs>
|
||||
<small>{{document.created | customDate:'mediumDate'}}</small>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div class="ps-0 p-1" placement="top" [ngbTooltip]="dateTooltip">
|
||||
<i-bs width="1em" height="1em" class="me-2 text-muted" name="calendar-event"></i-bs>
|
||||
<small>{{document.created_date | customDate:'mediumDate'}}</small>
|
||||
</div>
|
||||
</div>
|
||||
@if (document.archive_serial_number | isNumber) {
|
||||
}
|
||||
@if (displayFields.includes(DisplayField.ADDED)) {
|
||||
<div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between">
|
||||
<ng-template #dateTooltip>
|
||||
<div class="d-flex flex-column text-light">
|
||||
<span i18n>Created: {{ document.created | customDate }}</span>
|
||||
<span i18n>Added: {{ document.added | customDate }}</span>
|
||||
<span i18n>Modified: {{ document.modified | customDate }}</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div class="ps-0 p-1" placement="top" [ngbTooltip]="dateTooltip">
|
||||
<i-bs width="1em" height="1em" class="me-2 text-muted" name="calendar-event"></i-bs>
|
||||
<small>{{document.added | customDate:'mediumDate'}}</small>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (displayFields.includes(DisplayField.ASN) && document.archive_serial_number | isNumber) {
|
||||
<div class="ps-0 p-1">
|
||||
<i-bs width="1em" height="1em" class="me-2 text-muted" name="upc-scan"></i-bs>
|
||||
<small>#{{document.archive_serial_number}}</small>
|
||||
</div>
|
||||
}
|
||||
@if (document.owner && document.owner !== settingsService.currentUser.id) {
|
||||
@if (displayFields.includes(DisplayField.OWNER) && document.owner && document.owner !== settingsService.currentUser.id) {
|
||||
<div class="ps-0 p-1">
|
||||
<i-bs width="1em" height="1em" class="me-2 text-muted" name="person-fill-lock"></i-bs>
|
||||
<small>{{document.owner | username}}</small>
|
||||
</div>
|
||||
}
|
||||
@if (document.is_shared_by_requester) {
|
||||
@if (displayFields.includes(DisplayField.SHARED) && document.is_shared_by_requester) {
|
||||
<div class="ps-0 p-1">
|
||||
<i-bs width="1em" height="1em" class="me-2 text-muted" name="people-fill"></i-bs>
|
||||
<small i18n>Shared</small>
|
||||
</div>
|
||||
}
|
||||
@for (field of document.custom_fields; track field.id) {
|
||||
@if (displayFields.includes(DisplayField.CUSTOM_FIELD + field.field)) {
|
||||
<div class="ps-0 p-1 d-flex align-items-center overflow-hidden">
|
||||
<i-bs width="1em" height="1em" class="me-2 text-muted" name="ui-radios"></i-bs>
|
||||
<small><pngx-custom-field-display [document]="document" [fieldId]="field.field"></pngx-custom-field-display></small>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="btn-group w-100">
|
||||
|
@@ -24,6 +24,7 @@ import { Tag } from 'src/app/data/tag'
|
||||
import { IsNumberPipe } from 'src/app/pipes/is-number.pipe'
|
||||
import { PreviewPopupComponent } from '../../common/preview-popup/preview-popup.component'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { CustomFieldDisplayComponent } from '../../common/custom-field-display/custom-field-display.component'
|
||||
|
||||
const doc = {
|
||||
id: 10,
|
||||
@@ -67,6 +68,7 @@ describe('DocumentCardSmallComponent', () => {
|
||||
TagComponent,
|
||||
IsNumberPipe,
|
||||
PreviewPopupComponent,
|
||||
CustomFieldDisplayComponent,
|
||||
],
|
||||
providers: [DatePipe],
|
||||
imports: [
|
||||
|
@@ -6,7 +6,11 @@ import {
|
||||
ViewChild,
|
||||
} from '@angular/core'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { Document } from 'src/app/data/document'
|
||||
import {
|
||||
DEFAULT_DISPLAY_FIELDS,
|
||||
DisplayField,
|
||||
Document,
|
||||
} from 'src/app/data/document'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
||||
@@ -19,6 +23,8 @@ import { ComponentWithPermissions } from '../../with-permissions/with-permission
|
||||
styleUrls: ['./document-card-small.component.scss'],
|
||||
})
|
||||
export class DocumentCardSmallComponent extends ComponentWithPermissions {
|
||||
DisplayField = DisplayField
|
||||
|
||||
constructor(
|
||||
private documentService: DocumentService,
|
||||
public settingsService: SettingsService
|
||||
@@ -35,6 +41,9 @@ export class DocumentCardSmallComponent extends ComponentWithPermissions {
|
||||
@Input()
|
||||
document: Document
|
||||
|
||||
@Input()
|
||||
displayFields: string[] = DEFAULT_DISPLAY_FIELDS.map((f) => f.id)
|
||||
|
||||
@Output()
|
||||
dblClickDocument = new EventEmitter()
|
||||
|
||||
|
@@ -11,16 +11,32 @@
|
||||
<button ngbDropdownItem (click)="list.selectAll()" i18n>Select all</button>
|
||||
</div>
|
||||
</div>
|
||||
<div ngbDropdown class="d-flex">
|
||||
<button class="btn btn-sm btn-outline-primary" id="dropdownDisplayFields" ngbDropdownToggle>
|
||||
<i-bs name="card-heading"></i-bs>
|
||||
<div class="d-none d-sm-inline"> <ng-container i18n>Show</ng-container></div>
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="dropdownDisplayFields" class="shadow">
|
||||
<div class="px-3">
|
||||
@for (field of settingsService.allDisplayFields; track field.id) {
|
||||
<div class="form-check my-1">
|
||||
<input class="form-check-input mt-1" type="checkbox" id="displayField{{field.id}}" [checked]="activeDisplayFields.includes(field.id)" (change)="toggleDisplayField(field.id)">
|
||||
<label class="form-check-label" for="displayField{{field.id}}">{{field.name}}</label>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group flex-fill" role="group">
|
||||
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="details" (ngModelChange)="saveDisplayMode()" id="displayModeDetails" name="displayModeDetails">
|
||||
<input type="radio" class="btn-check" [(ngModel)]="list.displayMode" value="table" id="displayModeDetails" name="displayModeDetails">
|
||||
<label for="displayModeDetails" class="btn btn-outline-primary btn-sm">
|
||||
<i-bs name="list-ul"></i-bs>
|
||||
</label>
|
||||
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="smallCards" (ngModelChange)="saveDisplayMode()" id="displayModeSmall" name="displayModeSmall">
|
||||
<input type="radio" class="btn-check" [(ngModel)]="list.displayMode" value="smallCards" id="displayModeSmall" name="displayModeSmall">
|
||||
<label for="displayModeSmall" class="btn btn-outline-primary btn-sm">
|
||||
<i-bs name="grid"></i-bs>
|
||||
</label>
|
||||
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="largeCards" (ngModelChange)="saveDisplayMode()" id="displayModeLarge" name="displayModeLarge">
|
||||
<input type="radio" class="btn-check" [(ngModel)]="list.displayMode" value="largeCards" id="displayModeLarge" name="displayModeLarge">
|
||||
<label for="displayModeLarge" class="btn btn-outline-primary btn-sm">
|
||||
<i-bs name="hdd-stack"></i-bs>
|
||||
</label>
|
||||
@@ -41,7 +57,7 @@
|
||||
</div>
|
||||
<div>
|
||||
@for (f of getSortFields(); track f) {
|
||||
<button ngbDropdownItem (click)="setSortField(f.field)"
|
||||
<button ngbDropdownItem (click)="list.sortField = f.field"
|
||||
[class.active]="list.sortField === f.field">{{f.name}}
|
||||
</button>
|
||||
}
|
||||
@@ -109,7 +125,7 @@
|
||||
}
|
||||
</div>
|
||||
@if (list.collectionSize) {
|
||||
<ngb-pagination [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
|
||||
<ngb-pagination [pageSize]="list.pageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
|
||||
[rotate]="true" aria-label="Default pagination" size="sm"></ngb-pagination>
|
||||
}
|
||||
</div>
|
||||
@@ -122,48 +138,68 @@
|
||||
@if (list.error ) {
|
||||
<div class="alert alert-danger" role="alert"><ng-container i18n>Error while loading documents</ng-container>: {{list.error}}</div>
|
||||
} @else {
|
||||
@if (displayMode === 'largeCards') {
|
||||
@if (list.displayMode === DisplayMode.LARGE_CARDS) {
|
||||
<div>
|
||||
@for (d of list.documents; track trackByDocumentId($index, d)) {
|
||||
<pngx-document-card-large [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" (dblClickDocument)="openDocumentDetail(d)" [document]="d" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickDocumentType)="clickDocumentType($event)" (clickStoragePath)="clickStoragePath($event)" (clickMoreLike)="clickMoreLike(d.id)">
|
||||
<pngx-document-card-large
|
||||
[selected]="list.isSelected(d)"
|
||||
(toggleSelected)="toggleSelected(d, $event)"
|
||||
(dblClickDocument)="openDocumentDetail(d)"
|
||||
[document]="d"
|
||||
[displayFields]="activeDisplayFields"
|
||||
(clickTag)="clickTag($event)"
|
||||
(clickCorrespondent)="clickCorrespondent($event)"
|
||||
(clickDocumentType)="clickDocumentType($event)"
|
||||
(clickStoragePath)="clickStoragePath($event)"
|
||||
(clickMoreLike)="clickMoreLike(d.id)">
|
||||
</pngx-document-card-large>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (displayMode === 'details') {
|
||||
@if (list.displayMode === DisplayMode.TABLE) {
|
||||
<table class="table table-sm align-middle border shadow-sm">
|
||||
<thead>
|
||||
<th></th>
|
||||
<th class="d-none d-lg-table-cell"
|
||||
pngxSortable="archive_serial_number"
|
||||
title="Sort by ASN" i18n-title
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>ASN</th>
|
||||
<th class="d-none d-md-table-cell"
|
||||
pngxSortable="correspondent__name"
|
||||
title="Sort by correspondent" i18n-title
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Correspondent</th>
|
||||
<th
|
||||
pngxSortable="title"
|
||||
title="Sort by title" i18n-title
|
||||
class="w-40"
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Title</th>
|
||||
<th class="d-none d-xl-table-cell"
|
||||
pngxSortable="owner"
|
||||
title="Sort by owner" i18n-title
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Owner</th>
|
||||
@if (notesEnabled) {
|
||||
@if (activeDisplayFields.includes(DisplayField.ASN)) {
|
||||
<th class="d-none d-lg-table-cell"
|
||||
pngxSortable="archive_serial_number"
|
||||
title="Sort by ASN" i18n-title
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>ASN</th>
|
||||
}
|
||||
@if (activeDisplayFields.includes(DisplayField.CORRESPONDENT) && permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
|
||||
<th class="d-none d-md-table-cell"
|
||||
pngxSortable="correspondent__name"
|
||||
title="Sort by correspondent" i18n-title
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Correspondent</th>
|
||||
}
|
||||
@if (activeDisplayFields.includes(DisplayField.TITLE)) {
|
||||
<th
|
||||
pngxSortable="title"
|
||||
title="Sort by title" i18n-title
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Title</th>
|
||||
}
|
||||
@if (activeDisplayFields.includes(DisplayField.TAGS) && !activeDisplayFields.includes(DisplayField.TITLE)) {
|
||||
<th i18n>Tags</th>
|
||||
}
|
||||
@if (activeDisplayFields.includes(DisplayField.OWNER) && permissionService.currentUserCan(PermissionAction.View, PermissionType.User)) {
|
||||
<th class="d-none d-xl-table-cell"
|
||||
pngxSortable="owner"
|
||||
title="Sort by owner" i18n-title
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Owner</th>
|
||||
}
|
||||
@if (activeDisplayFields.includes(DisplayField.NOTES) && notesEnabled) {
|
||||
<th class="d-none d-xl-table-cell"
|
||||
pngxSortable="num_notes"
|
||||
title="Sort by notes" i18n-title
|
||||
@@ -172,34 +208,52 @@
|
||||
(sort)="onSort($event)"
|
||||
i18n>Notes</th>
|
||||
}
|
||||
<th class="d-none d-xl-table-cell"
|
||||
pngxSortable="document_type__name"
|
||||
title="Sort by document type" i18n-title
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Document type</th>
|
||||
<th class="d-none d-xl-table-cell"
|
||||
pngxSortable="storage_path__name"
|
||||
title="Sort by storage path" i18n-title
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Storage path</th>
|
||||
<th
|
||||
pngxSortable="created"
|
||||
title="Sort by created date" i18n-title
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Created</th>
|
||||
<th class="d-none d-xl-table-cell"
|
||||
pngxSortable="added"
|
||||
title="Sort by added date" i18n-title
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Added</th>
|
||||
@if (activeDisplayFields.includes(DisplayField.DOCUMENT_TYPE) && permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
|
||||
<th class="d-none d-xl-table-cell"
|
||||
pngxSortable="document_type__name"
|
||||
title="Sort by document type" i18n-title
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Document type</th>
|
||||
}
|
||||
@if (activeDisplayFields.includes(DisplayField.STORAGE_PATH) && permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
|
||||
<th class="d-none d-xl-table-cell"
|
||||
pngxSortable="storage_path__name"
|
||||
title="Sort by storage path" i18n-title
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Storage path</th>
|
||||
}
|
||||
@if (activeDisplayFields.includes(DisplayField.CREATED)) {
|
||||
<th
|
||||
pngxSortable="created"
|
||||
title="Sort by created date" i18n-title
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Created</th>
|
||||
}
|
||||
@if (activeDisplayFields.includes(DisplayField.ADDED)) {
|
||||
<th
|
||||
pngxSortable="added"
|
||||
title="Sort by added date" i18n-title
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Added</th>
|
||||
}
|
||||
@if (activeDisplayFields.includes(DisplayField.SHARED)) {
|
||||
<th i18n>
|
||||
Shared
|
||||
</th>
|
||||
}
|
||||
@for (field of activeDisplayCustomFields; track field) {
|
||||
<th>
|
||||
{{getDisplayCustomFieldTitle(field)}}
|
||||
</th>
|
||||
}
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (d of list.documents; track trackByDocumentId($index, d)) {
|
||||
@@ -210,24 +264,36 @@
|
||||
<label class="form-check-label" for="docCheck{{d.id}}"></label>
|
||||
</div>
|
||||
</td>
|
||||
<td class="d-none d-lg-table-cell">
|
||||
{{d.archive_serial_number}}
|
||||
</td>
|
||||
<td class="d-none d-md-table-cell">
|
||||
@if (d.correspondent) {
|
||||
<a (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent" i18n-title>{{(d.correspondent$ | async)?.name}}</a>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
|
||||
@for (t of d.tags$ | async; track t) {
|
||||
<pngx-tag [tag]="t" class="ms-1" clickable="true" linkTitle="Filter by tag" i18n-linkTitle (click)="clickTag(t.id);$event.stopPropagation()"></pngx-tag>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
{{d.owner | username}}
|
||||
</td>
|
||||
@if (notesEnabled) {
|
||||
@if (activeDisplayFields.includes(DisplayField.ASN)) {
|
||||
<td class="d-none d-xl-table-cell">
|
||||
{{d.archive_serial_number}}
|
||||
</td>
|
||||
}
|
||||
@if (activeDisplayFields.includes(DisplayField.CORRESPONDENT) && permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
|
||||
<td class="d-none d-xl-table-cell">
|
||||
@if (d.correspondent) {
|
||||
<a (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent" i18n-title>{{(d.correspondent$ | async)?.name}}</a>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
@if (activeDisplayFields.includes(DisplayField.TITLE) || activeDisplayFields.includes(DisplayField.TAGS)) {
|
||||
<td>
|
||||
@if (activeDisplayFields.includes(DisplayField.TITLE)) {
|
||||
<a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
|
||||
}
|
||||
@if (activeDisplayFields.includes(DisplayField.TAGS)) {
|
||||
@for (t of d.tags$ | async; track t) {
|
||||
<pngx-tag [tag]="t" class="ms-1" clickable="true" linkTitle="Filter by tag" i18n-linkTitle (click)="clickTag(t.id);$event.stopPropagation()"></pngx-tag>
|
||||
}
|
||||
}
|
||||
</td>
|
||||
}
|
||||
@if (activeDisplayFields.includes(DisplayField.OWNER) && permissionService.currentUserCan(PermissionAction.View, PermissionType.User)) {
|
||||
<td>
|
||||
{{d.owner | username}}
|
||||
</td>
|
||||
}
|
||||
@if (activeDisplayFields.includes(DisplayField.NOTES) && notesEnabled) {
|
||||
<td class="d-none d-xl-table-cell">
|
||||
@if (d.notes.length) {
|
||||
<a routerLink="/documents/{{d.id}}/notes" class="btn btn-sm p-0">
|
||||
@@ -238,31 +304,59 @@
|
||||
}
|
||||
</td>
|
||||
}
|
||||
<td class="d-none d-xl-table-cell">
|
||||
@if (d.document_type) {
|
||||
<a (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type" i18n-title>{{(d.document_type$ | async)?.name}}</a>
|
||||
}
|
||||
</td>
|
||||
<td class="d-none d-xl-table-cell">
|
||||
@if (d.storage_path) {
|
||||
<a (click)="clickStoragePath(d.storage_path);$event.stopPropagation()" title="Filter by storage path" i18n-title>{{(d.storage_path$ | async)?.name}}</a>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
{{d.created_date | customDate}}
|
||||
</td>
|
||||
<td class="d-none d-xl-table-cell">
|
||||
{{d.added | customDate}}
|
||||
</td>
|
||||
@if (activeDisplayFields.includes(DisplayField.DOCUMENT_TYPE) && permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
|
||||
<td class="d-none d-xl-table-cell">
|
||||
@if (d.document_type) {
|
||||
<a (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type" i18n-title>{{(d.document_type$ | async)?.name}}</a>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
@if (activeDisplayFields.includes(DisplayField.STORAGE_PATH) && permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
|
||||
<td class="d-none d-xl-table-cell">
|
||||
@if (d.storage_path) {
|
||||
<a (click)="clickStoragePath(d.storage_path);$event.stopPropagation()" title="Filter by storage path" i18n-title>{{(d.storage_path$ | async)?.name}}</a>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
@if (activeDisplayFields.includes(DisplayField.CREATED)) {
|
||||
<td>
|
||||
{{d.created_date | customDate}}
|
||||
</td>
|
||||
}
|
||||
@if (activeDisplayFields.includes(DisplayField.ADDED)) {
|
||||
<td>
|
||||
{{d.added | customDate}}
|
||||
</td>
|
||||
}
|
||||
@if (activeDisplayFields.includes(DisplayField.SHARED)) {
|
||||
<td>
|
||||
@if (d.is_shared_by_requester) { <ng-container i18n>Yes</ng-container> } @else { <ng-container i18n>No</ng-container> }
|
||||
</td>
|
||||
}
|
||||
@for (field of activeDisplayCustomFields; track field) {
|
||||
<td class="d-none d-xl-table-cell">
|
||||
<pngx-custom-field-display [document]="d" [fieldDisplayKey]="field"></pngx-custom-field-display>
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
@if (displayMode === 'smallCards') {
|
||||
@if (list.displayMode === DisplayMode.SMALL_CARDS) {
|
||||
<div class="row row-cols-paperless-cards">
|
||||
@for (d of list.documents; track trackByDocumentId($index, d)) {
|
||||
<pngx-document-card-small class="p-0" [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" (dblClickDocument)="openDocumentDetail(d)" [document]="d" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickStoragePath)="clickStoragePath($event)" (clickDocumentType)="clickDocumentType($event)"></pngx-document-card-small>
|
||||
<pngx-document-card-small class="p-0"
|
||||
[selected]="list.isSelected(d)"
|
||||
(toggleSelected)="toggleSelected(d, $event)"
|
||||
(dblClickDocument)="openDocumentDetail(d)"
|
||||
[document]="d"
|
||||
(clickTag)="clickTag($event)"
|
||||
[displayFields]="activeDisplayFields"
|
||||
(clickCorrespondent)="clickCorrespondent($event)"
|
||||
(clickStoragePath)="clickStoragePath($event)"
|
||||
(clickDocumentType)="clickDocumentType($event)">
|
||||
</pngx-document-card-small>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
@@ -10,10 +10,6 @@ th {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
th.w-40 {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.table-row-selected {
|
||||
background-color: var(--pngx-primary-faded);
|
||||
}
|
||||
@@ -84,3 +80,7 @@ $paperless-card-breakpoints: (
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
pngx-page-header .dropdown-menu {
|
||||
--bs-dropdown-min-width: 12em;
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ import { RouterTestingModule } from '@angular/router/testing'
|
||||
import { routes } from 'src/app/app-routing.module'
|
||||
import { FilterEditorComponent } from './filter-editor/filter-editor.component'
|
||||
import { PermissionsFilterDropdownComponent } from '../common/permissions-filter-dropdown/permissions-filter-dropdown.component'
|
||||
import { DateDropdownComponent } from '../common/date-dropdown/date-dropdown.component'
|
||||
import { DatesDropdownComponent } from '../common/dates-dropdown/dates-dropdown.component'
|
||||
import { FilterableDropdownComponent } from '../common/filterable-dropdown/filterable-dropdown.component'
|
||||
import { PageHeaderComponent } from '../common/page-header/page-header.component'
|
||||
import { BulkEditorComponent } from './bulk-editor/bulk-editor.component'
|
||||
@@ -47,12 +47,13 @@ import { DocumentCardSmallComponent } from './document-card-small/document-card-
|
||||
import { DocumentCardLargeComponent } from './document-card-large/document-card-large.component'
|
||||
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
||||
import { UsernamePipe } from 'src/app/pipes/username.pipe'
|
||||
import { Document } from 'src/app/data/document'
|
||||
import {
|
||||
DOCUMENT_SORT_FIELDS,
|
||||
DOCUMENT_SORT_FIELDS_FULLTEXT,
|
||||
DocumentService,
|
||||
} from 'src/app/services/rest/document.service'
|
||||
DEFAULT_DISPLAY_FIELDS,
|
||||
DisplayField,
|
||||
DisplayMode,
|
||||
Document,
|
||||
} from 'src/app/data/document'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
|
||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||
import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component'
|
||||
@@ -64,6 +65,8 @@ import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
import { IsNumberPipe } from 'src/app/pipes/is-number.pipe'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
|
||||
const docs: Document[] = [
|
||||
{
|
||||
@@ -101,6 +104,7 @@ describe('DocumentListComponent', () => {
|
||||
let toastService: ToastService
|
||||
let modalService: NgbModal
|
||||
let settingsService: SettingsService
|
||||
let permissionService: PermissionsService
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
@@ -109,7 +113,7 @@ describe('DocumentListComponent', () => {
|
||||
PageHeaderComponent,
|
||||
FilterEditorComponent,
|
||||
FilterableDropdownComponent,
|
||||
DateDropdownComponent,
|
||||
DatesDropdownComponent,
|
||||
PermissionsFilterDropdownComponent,
|
||||
ToggleableDropdownButtonComponent,
|
||||
BulkEditorComponent,
|
||||
@@ -148,6 +152,7 @@ describe('DocumentListComponent', () => {
|
||||
NgbPopoverModule,
|
||||
NgbTooltipModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
NgSelectModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
@@ -160,21 +165,11 @@ describe('DocumentListComponent', () => {
|
||||
toastService = TestBed.inject(ToastService)
|
||||
modalService = TestBed.inject(NgbModal)
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
permissionService = TestBed.inject(PermissionsService)
|
||||
fixture = TestBed.createComponent(DocumentListComponent)
|
||||
component = fixture.componentInstance
|
||||
})
|
||||
|
||||
it('should load display mode from local storage', () => {
|
||||
window.localStorage.setItem('document-list:displayMode', 'largeCards')
|
||||
fixture.detectChanges()
|
||||
expect(component.displayMode).toEqual('largeCards')
|
||||
component.displayMode = 'smallCards'
|
||||
component.saveDisplayMode()
|
||||
expect(window.localStorage.getItem('document-list:displayMode')).toEqual(
|
||||
'smallCards'
|
||||
)
|
||||
})
|
||||
|
||||
it('should reload on new document consumed', () => {
|
||||
const reloadSpy = jest.spyOn(documentListService, 'reload')
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
@@ -194,7 +189,7 @@ describe('DocumentListComponent', () => {
|
||||
},
|
||||
]
|
||||
fixture.detectChanges()
|
||||
expect(component.getSortFields()).toEqual(DOCUMENT_SORT_FIELDS)
|
||||
expect(component.getSortFields()).toEqual(documentListService.sortFields)
|
||||
|
||||
documentListService.filterRules = [
|
||||
{
|
||||
@@ -203,7 +198,9 @@ describe('DocumentListComponent', () => {
|
||||
},
|
||||
]
|
||||
fixture.detectChanges()
|
||||
expect(component.getSortFields()).toEqual(DOCUMENT_SORT_FIELDS_FULLTEXT)
|
||||
expect(component.getSortFields()).toEqual(
|
||||
documentListService.sortFieldsFullText
|
||||
)
|
||||
})
|
||||
|
||||
it('should determine if filtered, support reset', () => {
|
||||
@@ -292,18 +289,18 @@ describe('DocumentListComponent', () => {
|
||||
const displayModeButtons = fixture.debugElement.queryAll(
|
||||
By.css('input[type="radio"]')
|
||||
)
|
||||
expect(component.displayMode).toEqual('smallCards')
|
||||
expect(component.list.displayMode).toEqual('smallCards')
|
||||
|
||||
displayModeButtons[0].nativeElement.checked = true
|
||||
displayModeButtons[0].triggerEventHandler('change')
|
||||
fixture.detectChanges()
|
||||
expect(component.displayMode).toEqual('details')
|
||||
expect(component.list.displayMode).toEqual('table')
|
||||
expect(fixture.debugElement.queryAll(By.css('tr'))).toHaveLength(3)
|
||||
|
||||
displayModeButtons[1].nativeElement.checked = true
|
||||
displayModeButtons[1].triggerEventHandler('change')
|
||||
fixture.detectChanges()
|
||||
expect(component.displayMode).toEqual('smallCards')
|
||||
expect(component.list.displayMode).toEqual('smallCards')
|
||||
expect(
|
||||
fixture.debugElement.queryAll(By.directive(DocumentCardSmallComponent))
|
||||
).toHaveLength(3)
|
||||
@@ -311,7 +308,7 @@ describe('DocumentListComponent', () => {
|
||||
displayModeButtons[2].nativeElement.checked = true
|
||||
displayModeButtons[2].triggerEventHandler('change')
|
||||
fixture.detectChanges()
|
||||
expect(component.displayMode).toEqual('largeCards')
|
||||
expect(component.list.displayMode).toEqual('largeCards')
|
||||
expect(
|
||||
fixture.debugElement.queryAll(By.directive(DocumentCardLargeComponent))
|
||||
).toHaveLength(3)
|
||||
@@ -322,7 +319,7 @@ describe('DocumentListComponent', () => {
|
||||
fixture.detectChanges()
|
||||
const sortDropdown = fixture.debugElement.queryAll(
|
||||
By.directive(NgbDropdown)
|
||||
)[1]
|
||||
)[2]
|
||||
const asnSortFieldButton = sortDropdown.query(By.directive(NgbDropdownItem))
|
||||
|
||||
asnSortFieldButton.triggerEventHandler('click')
|
||||
@@ -332,6 +329,7 @@ describe('DocumentListComponent', () => {
|
||||
})
|
||||
|
||||
it('should support setting sort field by table head', () => {
|
||||
component.activeDisplayFields = [DisplayField.ASN]
|
||||
jest.spyOn(documentListService, 'documents', 'get').mockReturnValue(docs)
|
||||
fixture.detectChanges()
|
||||
expect(documentListService.sortField).toEqual('created')
|
||||
@@ -342,7 +340,7 @@ describe('DocumentListComponent', () => {
|
||||
detailsDisplayModeButton.nativeElement.checked = true
|
||||
detailsDisplayModeButton.triggerEventHandler('change')
|
||||
fixture.detectChanges()
|
||||
expect(component.displayMode).toEqual('details')
|
||||
expect(component.list.displayMode).toEqual(DisplayMode.TABLE)
|
||||
|
||||
const sortTh = fixture.debugElement.query(By.directive(SortableDirective))
|
||||
sortTh.triggerEventHandler('click')
|
||||
@@ -425,6 +423,8 @@ describe('DocumentListComponent', () => {
|
||||
value: '20',
|
||||
},
|
||||
],
|
||||
display_mode: DisplayMode.SMALL_CARDS,
|
||||
display_fields: [DisplayField.TITLE],
|
||||
}
|
||||
jest.spyOn(savedViewService, 'getCached').mockReturnValue(of(view))
|
||||
const queryParams = { view: view.id.toString() }
|
||||
@@ -541,6 +541,42 @@ describe('DocumentListComponent', () => {
|
||||
expect(openModal.componentInstance.error).toEqual({ filter_rules: ['11'] })
|
||||
})
|
||||
|
||||
it('should detect saved view changes', () => {
|
||||
const view: SavedView = {
|
||||
id: 10,
|
||||
name: 'Saved View 10',
|
||||
sort_field: 'added',
|
||||
sort_reverse: true,
|
||||
filter_rules: [
|
||||
{
|
||||
rule_type: FILTER_HAS_TAGS_ANY,
|
||||
value: '20',
|
||||
},
|
||||
],
|
||||
page_size: 5,
|
||||
display_mode: DisplayMode.SMALL_CARDS,
|
||||
display_fields: [DisplayField.TITLE],
|
||||
}
|
||||
jest.spyOn(savedViewService, 'getCached').mockReturnValue(of(view))
|
||||
const queryParams = { view: view.id.toString() }
|
||||
jest
|
||||
.spyOn(activatedRoute, 'queryParamMap', 'get')
|
||||
.mockReturnValue(of(convertToParamMap(queryParams)))
|
||||
activatedRoute.snapshot.queryParams = queryParams
|
||||
router.routerState.snapshot.url = '/view/10/'
|
||||
fixture.detectChanges()
|
||||
expect(documentListService.activeSavedViewId).toEqual(10)
|
||||
|
||||
component.list.displayFields = [DisplayField.ASN]
|
||||
expect(component.savedViewIsModified).toBeTruthy()
|
||||
component.list.displayFields = [DisplayField.TITLE]
|
||||
expect(component.savedViewIsModified).toBeFalsy()
|
||||
component.list.displayMode = DisplayMode.TABLE
|
||||
expect(component.savedViewIsModified).toBeTruthy()
|
||||
component.list.displayMode = DisplayMode.SMALL_CARDS
|
||||
expect(component.savedViewIsModified).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should navigate to a document', () => {
|
||||
fixture.detectChanges()
|
||||
const routerSpy = jest.spyOn(router, 'navigate')
|
||||
@@ -548,18 +584,14 @@ describe('DocumentListComponent', () => {
|
||||
expect(routerSpy).toHaveBeenCalledWith(['documents', 99])
|
||||
})
|
||||
|
||||
it('should support checking if notes enabled to hide column', () => {
|
||||
it('should hide columns if no perms or notes disabled', () => {
|
||||
jest.spyOn(permissionService, 'currentUserCan').mockReturnValue(true)
|
||||
jest.spyOn(documentListService, 'documents', 'get').mockReturnValue(docs)
|
||||
fixture.detectChanges()
|
||||
expect(documentListService.sortField).toEqual('created')
|
||||
|
||||
const detailsDisplayModeButton = fixture.debugElement.query(
|
||||
By.css('input[type="radio"]')
|
||||
)
|
||||
detailsDisplayModeButton.nativeElement.checked = true
|
||||
detailsDisplayModeButton.triggerEventHandler('change')
|
||||
component.list.displayMode = DisplayMode.TABLE
|
||||
component.list.displayFields = DEFAULT_DISPLAY_FIELDS.map((f) => f.id)
|
||||
fixture.detectChanges()
|
||||
expect(component.displayMode).toEqual('details')
|
||||
|
||||
expect(
|
||||
fixture.debugElement.queryAll(By.directive(SortableDirective))
|
||||
@@ -572,6 +604,13 @@ describe('DocumentListComponent', () => {
|
||||
expect(
|
||||
fixture.debugElement.queryAll(By.directive(SortableDirective))
|
||||
).toHaveLength(8)
|
||||
|
||||
// insufficient perms
|
||||
jest.spyOn(permissionService, 'currentUserCan').mockReturnValue(false)
|
||||
fixture.detectChanges()
|
||||
expect(
|
||||
fixture.debugElement.queryAll(By.directive(SortableDirective))
|
||||
).toHaveLength(4)
|
||||
})
|
||||
|
||||
it('should support toggle on document objects', () => {
|
||||
@@ -591,4 +630,28 @@ describe('DocumentListComponent', () => {
|
||||
{ rule_type: FILTER_FULLTEXT_MORELIKE, value: '99' },
|
||||
])
|
||||
})
|
||||
|
||||
it('should support toggling display fields', () => {
|
||||
fixture.detectChanges()
|
||||
component.activeDisplayFields = [DisplayField.ASN]
|
||||
component.toggleDisplayField(DisplayField.TITLE)
|
||||
expect(component.activeDisplayFields).toEqual([
|
||||
DisplayField.ASN,
|
||||
DisplayField.TITLE,
|
||||
])
|
||||
component.toggleDisplayField(DisplayField.ASN)
|
||||
expect(component.activeDisplayFields).toEqual([DisplayField.TITLE])
|
||||
})
|
||||
|
||||
it('should get custom field title', () => {
|
||||
fixture.detectChanges()
|
||||
jest
|
||||
.spyOn(settingsService, 'allDisplayFields', 'get')
|
||||
.mockReturnValue([
|
||||
{ id: 'custom_field_1' as any, name: 'Custom Field 1' },
|
||||
])
|
||||
expect(component.getDisplayCustomFieldTitle('custom_field_1')).toEqual(
|
||||
'Custom Field 1'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@@ -15,7 +15,7 @@ import {
|
||||
isFullTextFilterRule,
|
||||
} from 'src/app/utils/filter-rules'
|
||||
import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type'
|
||||
import { Document } from 'src/app/data/document'
|
||||
import { DisplayField, DisplayMode, Document } from 'src/app/data/document'
|
||||
import { SavedView } from 'src/app/data/saved-view'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
import {
|
||||
@@ -25,10 +25,7 @@ import {
|
||||
import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||
import {
|
||||
DOCUMENT_SORT_FIELDS,
|
||||
DOCUMENT_SORT_FIELDS_FULLTEXT,
|
||||
} from 'src/app/services/rest/document.service'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
@@ -45,6 +42,9 @@ export class DocumentListComponent
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
DisplayField = DisplayField
|
||||
DisplayMode = DisplayMode
|
||||
|
||||
constructor(
|
||||
public list: DocumentListViewService,
|
||||
public savedViewService: SavedViewService,
|
||||
@@ -54,7 +54,8 @@ export class DocumentListComponent
|
||||
private modalService: NgbModal,
|
||||
private consumerStatusService: ConsumerStatusService,
|
||||
public openDocumentsService: OpenDocumentsService,
|
||||
private settingsService: SettingsService
|
||||
public settingsService: SettingsService,
|
||||
public permissionService: PermissionsService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@@ -64,7 +65,25 @@ export class DocumentListComponent
|
||||
|
||||
@ViewChildren(SortableDirective) headers: QueryList<SortableDirective>
|
||||
|
||||
displayMode = 'smallCards' // largeCards, smallCards, details
|
||||
get activeDisplayFields(): DisplayField[] {
|
||||
return this.list.displayFields
|
||||
}
|
||||
|
||||
set activeDisplayFields(fields: DisplayField[]) {
|
||||
this.list.displayFields = fields
|
||||
this.updateDisplayCustomFields()
|
||||
}
|
||||
activeDisplayCustomFields: Set<string> = new Set()
|
||||
|
||||
public updateDisplayCustomFields() {
|
||||
this.activeDisplayCustomFields = new Set(
|
||||
Array.from(this.activeDisplayFields).filter(
|
||||
(field) =>
|
||||
typeof field === 'string' &&
|
||||
field.startsWith(DisplayField.CUSTOM_FIELD)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
unmodifiedFilterRules: FilterRule[] = []
|
||||
private unmodifiedSavedView: SavedView
|
||||
@@ -77,6 +96,16 @@ export class DocumentListComponent
|
||||
return (
|
||||
this.unmodifiedSavedView.sort_field !== this.list.sortField ||
|
||||
this.unmodifiedSavedView.sort_reverse !== this.list.sortReverse ||
|
||||
(this.unmodifiedSavedView.page_size &&
|
||||
this.unmodifiedSavedView.page_size !== this.list.pageSize) ||
|
||||
(this.unmodifiedSavedView.display_mode &&
|
||||
this.unmodifiedSavedView.display_mode !== this.list.displayMode) ||
|
||||
// if the saved view has no display mode, we assume it's small cards
|
||||
(!this.unmodifiedSavedView.display_mode &&
|
||||
this.list.displayMode !== DisplayMode.SMALL_CARDS) ||
|
||||
(this.unmodifiedSavedView.display_fields &&
|
||||
this.unmodifiedSavedView.display_fields.join(',') !==
|
||||
this.activeDisplayFields.join(',')) ||
|
||||
filterRulesDiffer(
|
||||
this.unmodifiedSavedView.filter_rules,
|
||||
this.list.filterRules
|
||||
@@ -101,8 +130,8 @@ export class DocumentListComponent
|
||||
|
||||
getSortFields() {
|
||||
return isFullTextFilterRule(this.list.filterRules)
|
||||
? DOCUMENT_SORT_FIELDS_FULLTEXT
|
||||
: DOCUMENT_SORT_FIELDS
|
||||
? this.list.sortFieldsFullText
|
||||
: this.list.sortFields
|
||||
}
|
||||
|
||||
set listSortReverse(reverse: boolean) {
|
||||
@@ -113,10 +142,6 @@ export class DocumentListComponent
|
||||
return this.list.sortReverse
|
||||
}
|
||||
|
||||
setSortField(field: string) {
|
||||
this.list.sortField = field
|
||||
}
|
||||
|
||||
onSort(event: SortEvent) {
|
||||
this.list.setSort(event.column, event.reverse)
|
||||
}
|
||||
@@ -125,15 +150,23 @@ export class DocumentListComponent
|
||||
return this.list.selected.size > 0
|
||||
}
|
||||
|
||||
saveDisplayMode() {
|
||||
localStorage.setItem('document-list:displayMode', this.displayMode)
|
||||
toggleDisplayField(field: DisplayField) {
|
||||
if (this.activeDisplayFields.includes(field)) {
|
||||
this.activeDisplayFields = this.activeDisplayFields.filter(
|
||||
(f) => f !== field
|
||||
)
|
||||
} else {
|
||||
this.activeDisplayFields = [...this.activeDisplayFields, field]
|
||||
}
|
||||
this.updateDisplayCustomFields()
|
||||
}
|
||||
|
||||
public getDisplayCustomFieldTitle(field: string) {
|
||||
return this.settingsService.allDisplayFields.find((f) => f.id === field)
|
||||
?.name
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (localStorage.getItem('document-list:displayMode') != null) {
|
||||
this.displayMode = localStorage.getItem('document-list:displayMode')
|
||||
}
|
||||
|
||||
this.consumerStatusService
|
||||
.onDocumentConsumptionFinished()
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
@@ -197,6 +230,8 @@ export class DocumentListComponent
|
||||
filter_rules: this.list.filterRules,
|
||||
sort_field: this.list.sortField,
|
||||
sort_reverse: this.list.sortReverse,
|
||||
display_mode: this.list.displayMode,
|
||||
display_fields: this.activeDisplayFields,
|
||||
}
|
||||
this.savedViewService
|
||||
.patch(savedView)
|
||||
@@ -236,6 +271,8 @@ export class DocumentListComponent
|
||||
filter_rules: this.list.filterRules,
|
||||
sort_reverse: this.list.sortReverse,
|
||||
sort_field: this.list.sortField,
|
||||
display_mode: this.list.displayMode,
|
||||
display_fields: this.activeDisplayFields,
|
||||
}
|
||||
|
||||
this.savedViewService
|
||||
|
@@ -70,22 +70,28 @@
|
||||
[documentCounts]="storagePathDocumentCounts"
|
||||
[allowSelectNone]="true"></pngx-filterable-dropdown>
|
||||
}
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<pngx-date-dropdown
|
||||
title="Created" i18n-title
|
||||
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.CustomField)) {
|
||||
<pngx-filterable-dropdown class="flex-fill" title="Custom fields" icon="ui-radios" i18n-title
|
||||
filterPlaceholder="Filter custom fields" i18n-filterPlaceholder
|
||||
[items]="customFields"
|
||||
[manyToOne]="true"
|
||||
[(selectionModel)]="customFieldSelectionModel"
|
||||
(selectionModelChange)="updateRules()"
|
||||
(opened)="onCustomFieldsDropdownOpen()"
|
||||
[documentCounts]="customFieldDocumentCounts"
|
||||
[allowSelectNone]="true"></pngx-filterable-dropdown>
|
||||
}
|
||||
<pngx-dates-dropdown
|
||||
title="Dates" i18n-title
|
||||
(datesSet)="updateRules()"
|
||||
[(dateBefore)]="dateCreatedBefore"
|
||||
[(dateAfter)]="dateCreatedAfter"
|
||||
[(relativeDate)]="dateCreatedRelativeDate"></pngx-date-dropdown>
|
||||
<pngx-date-dropdown
|
||||
title="Added" i18n-title
|
||||
(datesSet)="updateRules()"
|
||||
[(dateBefore)]="dateAddedBefore"
|
||||
[(dateAfter)]="dateAddedAfter"
|
||||
[(relativeDate)]="dateAddedRelativeDate"></pngx-date-dropdown>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap">
|
||||
[(createdDateBefore)]="dateCreatedBefore"
|
||||
[(createdDateAfter)]="dateCreatedAfter"
|
||||
[(createdRelativeDate)]="dateCreatedRelativeDate"
|
||||
[(addedDateBefore)]="dateAddedBefore"
|
||||
[(addedDateAfter)]="dateAddedAfter"
|
||||
[(addedRelativeDate)]="dateAddedRelativeDate">
|
||||
</pngx-dates-dropdown>
|
||||
<pngx-permissions-filter-dropdown
|
||||
title="Permissions" i18n-title
|
||||
(ownerFilterSet)="updateRules()"
|
||||
|
@@ -49,8 +49,12 @@ import {
|
||||
FILTER_OWNER_ANY,
|
||||
FILTER_OWNER_DOES_NOT_INCLUDE,
|
||||
FILTER_OWNER_ISNULL,
|
||||
FILTER_CUSTOM_FIELDS,
|
||||
FILTER_CUSTOM_FIELDS_TEXT,
|
||||
FILTER_SHARED_BY_USER,
|
||||
FILTER_HAS_CUSTOM_FIELDS_ANY,
|
||||
FILTER_HAS_ANY_CUSTOM_FIELDS,
|
||||
FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS,
|
||||
FILTER_HAS_CUSTOM_FIELDS_ALL,
|
||||
} from 'src/app/data/filter-rule-type'
|
||||
import { Correspondent } from 'src/app/data/correspondent'
|
||||
import { DocumentType } from 'src/app/data/document-type'
|
||||
@@ -68,7 +72,7 @@ import { TagService } from 'src/app/services/rest/tag.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
|
||||
import { DateDropdownComponent } from '../../common/date-dropdown/date-dropdown.component'
|
||||
import { DatesDropdownComponent } from '../../common/dates-dropdown/dates-dropdown.component'
|
||||
import {
|
||||
FilterableDropdownComponent,
|
||||
LogicalOperator,
|
||||
@@ -86,6 +90,8 @@ import {
|
||||
PermissionsService,
|
||||
} from 'src/app/services/permissions.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
|
||||
const tags: Tag[] = [
|
||||
{
|
||||
@@ -131,6 +137,19 @@ const storage_paths: StoragePath[] = [
|
||||
},
|
||||
]
|
||||
|
||||
const custom_fields: CustomField[] = [
|
||||
{
|
||||
id: 42,
|
||||
data_type: CustomFieldDataType.String,
|
||||
name: 'CustomField42',
|
||||
},
|
||||
{
|
||||
id: 43,
|
||||
data_type: CustomFieldDataType.String,
|
||||
name: 'CustomField43',
|
||||
},
|
||||
]
|
||||
|
||||
const users: User[] = [
|
||||
{
|
||||
id: 1,
|
||||
@@ -156,7 +175,7 @@ describe('FilterEditorComponent', () => {
|
||||
IfPermissionsDirective,
|
||||
ClearableBadgeComponent,
|
||||
ToggleableDropdownButtonComponent,
|
||||
DateDropdownComponent,
|
||||
DatesDropdownComponent,
|
||||
CustomDatePipe,
|
||||
],
|
||||
providers: [
|
||||
@@ -187,6 +206,12 @@ describe('FilterEditorComponent', () => {
|
||||
listAll: () => of({ results: storage_paths }),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CustomFieldsService,
|
||||
useValue: {
|
||||
listAll: () => of({ results: custom_fields }),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {
|
||||
@@ -285,7 +310,7 @@ describe('FilterEditorComponent', () => {
|
||||
expect(component.textFilter).toEqual(null)
|
||||
component.filterRules = [
|
||||
{
|
||||
rule_type: FILTER_CUSTOM_FIELDS,
|
||||
rule_type: FILTER_CUSTOM_FIELDS_TEXT,
|
||||
value: 'foo',
|
||||
},
|
||||
]
|
||||
@@ -806,6 +831,110 @@ describe('FilterEditorComponent', () => {
|
||||
]
|
||||
}))
|
||||
|
||||
it('should ingest filter rules for has all custom fields', fakeAsync(() => {
|
||||
expect(component.customFieldSelectionModel.getSelectedItems()).toHaveLength(
|
||||
0
|
||||
)
|
||||
component.filterRules = [
|
||||
{
|
||||
rule_type: FILTER_HAS_CUSTOM_FIELDS_ALL,
|
||||
value: '42',
|
||||
},
|
||||
{
|
||||
rule_type: FILTER_HAS_CUSTOM_FIELDS_ALL,
|
||||
value: '43',
|
||||
},
|
||||
]
|
||||
expect(component.customFieldSelectionModel.logicalOperator).toEqual(
|
||||
LogicalOperator.And
|
||||
)
|
||||
expect(component.customFieldSelectionModel.getSelectedItems()).toEqual(
|
||||
custom_fields
|
||||
)
|
||||
// coverage
|
||||
component.filterRules = [
|
||||
{
|
||||
rule_type: FILTER_HAS_CUSTOM_FIELDS_ALL,
|
||||
value: null,
|
||||
},
|
||||
]
|
||||
component.toggleTag(2) // coverage
|
||||
}))
|
||||
|
||||
it('should ingest filter rules for has any custom fields', fakeAsync(() => {
|
||||
expect(component.customFieldSelectionModel.getSelectedItems()).toHaveLength(
|
||||
0
|
||||
)
|
||||
component.filterRules = [
|
||||
{
|
||||
rule_type: FILTER_HAS_CUSTOM_FIELDS_ANY,
|
||||
value: '42',
|
||||
},
|
||||
{
|
||||
rule_type: FILTER_HAS_CUSTOM_FIELDS_ANY,
|
||||
value: '43',
|
||||
},
|
||||
]
|
||||
expect(component.customFieldSelectionModel.logicalOperator).toEqual(
|
||||
LogicalOperator.Or
|
||||
)
|
||||
expect(component.customFieldSelectionModel.getSelectedItems()).toEqual(
|
||||
custom_fields
|
||||
)
|
||||
// coverage
|
||||
component.filterRules = [
|
||||
{
|
||||
rule_type: FILTER_HAS_CUSTOM_FIELDS_ANY,
|
||||
value: null,
|
||||
},
|
||||
]
|
||||
}))
|
||||
|
||||
it('should ingest filter rules for has any custom field', fakeAsync(() => {
|
||||
expect(component.customFieldSelectionModel.getSelectedItems()).toHaveLength(
|
||||
0
|
||||
)
|
||||
component.filterRules = [
|
||||
{
|
||||
rule_type: FILTER_HAS_ANY_CUSTOM_FIELDS,
|
||||
value: '1',
|
||||
},
|
||||
]
|
||||
expect(component.customFieldSelectionModel.getSelectedItems()).toHaveLength(
|
||||
1
|
||||
)
|
||||
expect(component.customFieldSelectionModel.get(null)).toBeTruthy()
|
||||
}))
|
||||
|
||||
it('should ingest filter rules for exclude tag(s)', fakeAsync(() => {
|
||||
expect(component.customFieldSelectionModel.getExcludedItems()).toHaveLength(
|
||||
0
|
||||
)
|
||||
component.filterRules = [
|
||||
{
|
||||
rule_type: FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS,
|
||||
value: '42',
|
||||
},
|
||||
{
|
||||
rule_type: FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS,
|
||||
value: '43',
|
||||
},
|
||||
]
|
||||
expect(component.customFieldSelectionModel.logicalOperator).toEqual(
|
||||
LogicalOperator.And
|
||||
)
|
||||
expect(component.customFieldSelectionModel.getExcludedItems()).toEqual(
|
||||
custom_fields
|
||||
)
|
||||
// coverage
|
||||
component.filterRules = [
|
||||
{
|
||||
rule_type: FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS,
|
||||
value: null,
|
||||
},
|
||||
]
|
||||
}))
|
||||
|
||||
it('should ingest filter rules for owner', fakeAsync(() => {
|
||||
expect(component.permissionsSelectionModel.ownerFilter).toEqual(
|
||||
OwnerFilterType.NONE
|
||||
@@ -1053,7 +1182,7 @@ describe('FilterEditorComponent', () => {
|
||||
expect(component.textFilterTarget).toEqual('custom-fields')
|
||||
expect(component.filterRules).toEqual([
|
||||
{
|
||||
rule_type: FILTER_CUSTOM_FIELDS,
|
||||
rule_type: FILTER_CUSTOM_FIELDS_TEXT,
|
||||
value: 'foo',
|
||||
},
|
||||
])
|
||||
@@ -1317,9 +1446,78 @@ describe('FilterEditorComponent', () => {
|
||||
])
|
||||
}))
|
||||
|
||||
it('should convert user input to correct filter rules on custom field select not assigned', fakeAsync(() => {
|
||||
const customFieldsFilterableDropdown = fixture.debugElement.queryAll(
|
||||
By.directive(FilterableDropdownComponent)
|
||||
)[4]
|
||||
customFieldsFilterableDropdown.triggerEventHandler('opened')
|
||||
const customFieldButton = customFieldsFilterableDropdown.queryAll(
|
||||
By.directive(ToggleableDropdownButtonComponent)
|
||||
)[0]
|
||||
customFieldButton.triggerEventHandler('toggle')
|
||||
fixture.detectChanges()
|
||||
expect(component.filterRules).toEqual([
|
||||
{
|
||||
rule_type: FILTER_HAS_ANY_CUSTOM_FIELDS,
|
||||
value: 'false',
|
||||
},
|
||||
])
|
||||
}))
|
||||
|
||||
it('should convert user input to correct filter rules on custom field selections', fakeAsync(() => {
|
||||
const customFieldsFilterableDropdown = fixture.debugElement.queryAll(
|
||||
By.directive(FilterableDropdownComponent)
|
||||
)[4] // CF dropdown
|
||||
customFieldsFilterableDropdown.triggerEventHandler('opened')
|
||||
const customFieldButtons = customFieldsFilterableDropdown.queryAll(
|
||||
By.directive(ToggleableDropdownButtonComponent)
|
||||
)
|
||||
customFieldButtons[1].triggerEventHandler('toggle')
|
||||
customFieldButtons[2].triggerEventHandler('toggle')
|
||||
fixture.detectChanges()
|
||||
expect(component.filterRules).toEqual([
|
||||
{
|
||||
rule_type: FILTER_HAS_CUSTOM_FIELDS_ALL,
|
||||
value: custom_fields[0].id.toString(),
|
||||
},
|
||||
{
|
||||
rule_type: FILTER_HAS_CUSTOM_FIELDS_ALL,
|
||||
value: custom_fields[1].id.toString(),
|
||||
},
|
||||
])
|
||||
const toggleOperatorButtons = customFieldsFilterableDropdown.queryAll(
|
||||
By.css('input[type=radio]')
|
||||
)
|
||||
toggleOperatorButtons[1].nativeElement.checked = true
|
||||
toggleOperatorButtons[1].triggerEventHandler('change')
|
||||
fixture.detectChanges()
|
||||
expect(component.filterRules).toEqual([
|
||||
{
|
||||
rule_type: FILTER_HAS_CUSTOM_FIELDS_ANY,
|
||||
value: custom_fields[0].id.toString(),
|
||||
},
|
||||
{
|
||||
rule_type: FILTER_HAS_CUSTOM_FIELDS_ANY,
|
||||
value: custom_fields[1].id.toString(),
|
||||
},
|
||||
])
|
||||
customFieldButtons[2].triggerEventHandler('exclude')
|
||||
fixture.detectChanges()
|
||||
expect(component.filterRules).toEqual([
|
||||
{
|
||||
rule_type: FILTER_HAS_CUSTOM_FIELDS_ALL,
|
||||
value: custom_fields[0].id.toString(),
|
||||
},
|
||||
{
|
||||
rule_type: FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS,
|
||||
value: custom_fields[1].id.toString(),
|
||||
},
|
||||
])
|
||||
}))
|
||||
|
||||
it('should convert user input to correct filter rules on date created after', fakeAsync(() => {
|
||||
const dateCreatedDropdown = fixture.debugElement.queryAll(
|
||||
By.directive(DateDropdownComponent)
|
||||
By.directive(DatesDropdownComponent)
|
||||
)[0]
|
||||
const dateCreatedAfter = dateCreatedDropdown.queryAll(By.css('input'))[0]
|
||||
|
||||
@@ -1339,7 +1537,7 @@ describe('FilterEditorComponent', () => {
|
||||
|
||||
it('should convert user input to correct filter rules on date created before', fakeAsync(() => {
|
||||
const dateCreatedDropdown = fixture.debugElement.queryAll(
|
||||
By.directive(DateDropdownComponent)
|
||||
By.directive(DatesDropdownComponent)
|
||||
)[0]
|
||||
const dateCreatedBefore = dateCreatedDropdown.queryAll(By.css('input'))[1]
|
||||
|
||||
@@ -1359,7 +1557,7 @@ describe('FilterEditorComponent', () => {
|
||||
|
||||
it('should convert user input to correct filter rules on date created with relative date', fakeAsync(() => {
|
||||
const dateCreatedDropdown = fixture.debugElement.queryAll(
|
||||
By.directive(DateDropdownComponent)
|
||||
By.directive(DatesDropdownComponent)
|
||||
)[0]
|
||||
const dateCreatedBeforeRelativeButton = dateCreatedDropdown.queryAll(
|
||||
By.css('button')
|
||||
@@ -1378,7 +1576,7 @@ describe('FilterEditorComponent', () => {
|
||||
it('should carry over text filtering on date created with relative date', fakeAsync(() => {
|
||||
component.textFilter = 'foo'
|
||||
const dateCreatedDropdown = fixture.debugElement.queryAll(
|
||||
By.directive(DateDropdownComponent)
|
||||
By.directive(DatesDropdownComponent)
|
||||
)[0]
|
||||
const dateCreatedBeforeRelativeButton = dateCreatedDropdown.queryAll(
|
||||
By.css('button')
|
||||
@@ -1423,10 +1621,10 @@ describe('FilterEditorComponent', () => {
|
||||
}))
|
||||
|
||||
it('should convert user input to correct filter rules on date added after', fakeAsync(() => {
|
||||
const dateAddedDropdown = fixture.debugElement.queryAll(
|
||||
By.directive(DateDropdownComponent)
|
||||
)[1]
|
||||
const dateAddedAfter = dateAddedDropdown.queryAll(By.css('input'))[0]
|
||||
const datesDropdown = fixture.debugElement.query(
|
||||
By.directive(DatesDropdownComponent)
|
||||
)
|
||||
const dateAddedAfter = datesDropdown.queryAll(By.css('input'))[2]
|
||||
|
||||
dateAddedAfter.nativeElement.value = '05/14/2023'
|
||||
// dateAddedAfter.triggerEventHandler('change')
|
||||
@@ -1443,10 +1641,10 @@ describe('FilterEditorComponent', () => {
|
||||
}))
|
||||
|
||||
it('should convert user input to correct filter rules on date added before', fakeAsync(() => {
|
||||
const dateAddedDropdown = fixture.debugElement.queryAll(
|
||||
By.directive(DateDropdownComponent)
|
||||
)[1]
|
||||
const dateAddedBefore = dateAddedDropdown.queryAll(By.css('input'))[1]
|
||||
const datesDropdown = fixture.debugElement.query(
|
||||
By.directive(DatesDropdownComponent)
|
||||
)
|
||||
const dateAddedBefore = datesDropdown.queryAll(By.css('input'))[2]
|
||||
|
||||
dateAddedBefore.nativeElement.value = '05/14/2023'
|
||||
// dateAddedBefore.triggerEventHandler('change')
|
||||
@@ -1463,38 +1661,38 @@ describe('FilterEditorComponent', () => {
|
||||
}))
|
||||
|
||||
it('should convert user input to correct filter rules on date added with relative date', fakeAsync(() => {
|
||||
const dateAddedDropdown = fixture.debugElement.queryAll(
|
||||
By.directive(DateDropdownComponent)
|
||||
)[1]
|
||||
const dateAddedBeforeRelativeButton = dateAddedDropdown.queryAll(
|
||||
const datesDropdown = fixture.debugElement.query(
|
||||
By.directive(DatesDropdownComponent)
|
||||
)
|
||||
const dateCreatedBeforeRelativeButton = datesDropdown.queryAll(
|
||||
By.css('button')
|
||||
)[1]
|
||||
dateAddedBeforeRelativeButton.triggerEventHandler('click')
|
||||
dateCreatedBeforeRelativeButton.triggerEventHandler('click')
|
||||
fixture.detectChanges()
|
||||
tick(400)
|
||||
expect(component.filterRules).toEqual([
|
||||
{
|
||||
rule_type: FILTER_FULLTEXT_QUERY,
|
||||
value: 'added:[-1 week to now]',
|
||||
value: 'created:[-1 week to now]',
|
||||
},
|
||||
])
|
||||
}))
|
||||
|
||||
it('should carry over text filtering on date added with relative date', fakeAsync(() => {
|
||||
component.textFilter = 'foo'
|
||||
const dateAddedDropdown = fixture.debugElement.queryAll(
|
||||
By.directive(DateDropdownComponent)
|
||||
)[1]
|
||||
const dateAddedBeforeRelativeButton = dateAddedDropdown.queryAll(
|
||||
const datesDropdown = fixture.debugElement.query(
|
||||
By.directive(DatesDropdownComponent)
|
||||
)
|
||||
const dateCreatedBeforeRelativeButton = datesDropdown.queryAll(
|
||||
By.css('button')
|
||||
)[1]
|
||||
dateAddedBeforeRelativeButton.triggerEventHandler('click')
|
||||
dateCreatedBeforeRelativeButton.triggerEventHandler('click')
|
||||
fixture.detectChanges()
|
||||
tick(400)
|
||||
expect(component.filterRules).toEqual([
|
||||
{
|
||||
rule_type: FILTER_FULLTEXT_QUERY,
|
||||
value: 'foo,added:[-1 week to now]',
|
||||
value: 'foo,created:[-1 week to now]',
|
||||
},
|
||||
])
|
||||
}))
|
||||
@@ -1645,6 +1843,10 @@ describe('FilterEditorComponent', () => {
|
||||
{ id: 32, document_count: 1 },
|
||||
{ id: 33, document_count: 0 },
|
||||
],
|
||||
selected_custom_fields: [
|
||||
{ id: 42, document_count: 1 },
|
||||
{ id: 43, document_count: 0 },
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1719,6 +1921,24 @@ describe('FilterEditorComponent', () => {
|
||||
]
|
||||
expect(component.generateFilterName()).toEqual('Without any tag')
|
||||
|
||||
component.filterRules = [
|
||||
{
|
||||
rule_type: FILTER_HAS_CUSTOM_FIELDS_ALL,
|
||||
value: '42',
|
||||
},
|
||||
]
|
||||
expect(component.generateFilterName()).toEqual(
|
||||
`Custom fields: ${custom_fields[0].name}`
|
||||
)
|
||||
|
||||
component.filterRules = [
|
||||
{
|
||||
rule_type: FILTER_HAS_ANY_CUSTOM_FIELDS,
|
||||
value: 'false',
|
||||
},
|
||||
]
|
||||
expect(component.generateFilterName()).toEqual('Without any custom field')
|
||||
|
||||
component.filterRules = [
|
||||
{
|
||||
rule_type: FILTER_TITLE,
|
||||
|
@@ -48,8 +48,12 @@ import {
|
||||
FILTER_OWNER_DOES_NOT_INCLUDE,
|
||||
FILTER_OWNER_ISNULL,
|
||||
FILTER_OWNER_ANY,
|
||||
FILTER_CUSTOM_FIELDS,
|
||||
FILTER_CUSTOM_FIELDS_TEXT,
|
||||
FILTER_SHARED_BY_USER,
|
||||
FILTER_HAS_CUSTOM_FIELDS_ANY,
|
||||
FILTER_HAS_CUSTOM_FIELDS_ALL,
|
||||
FILTER_HAS_ANY_CUSTOM_FIELDS,
|
||||
FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS,
|
||||
} from 'src/app/data/filter-rule-type'
|
||||
import {
|
||||
FilterableDropdownSelectionModel,
|
||||
@@ -65,7 +69,7 @@ import {
|
||||
import { Document } from 'src/app/data/document'
|
||||
import { StoragePath } from 'src/app/data/storage-path'
|
||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||
import { RelativeDate } from '../../common/date-dropdown/date-dropdown.component'
|
||||
import { RelativeDate } from '../../common/dates-dropdown/dates-dropdown.component'
|
||||
import {
|
||||
OwnerFilterType,
|
||||
PermissionsSelectionModel,
|
||||
@@ -76,6 +80,8 @@ import {
|
||||
PermissionsService,
|
||||
} from 'src/app/services/permissions.service'
|
||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { CustomField } from 'src/app/data/custom-field'
|
||||
|
||||
const TEXT_FILTER_TARGET_TITLE = 'title'
|
||||
const TEXT_FILTER_TARGET_TITLE_CONTENT = 'title-content'
|
||||
@@ -208,6 +214,16 @@ export class FilterEditorComponent
|
||||
return $localize`Without any tag`
|
||||
}
|
||||
|
||||
case FILTER_HAS_CUSTOM_FIELDS_ALL:
|
||||
return $localize`Custom fields: ${this.customFields.find(
|
||||
(f) => f.id == +rule.value
|
||||
)?.name}`
|
||||
|
||||
case FILTER_HAS_ANY_CUSTOM_FIELDS:
|
||||
if (rule.value == 'false') {
|
||||
return $localize`Without any custom field`
|
||||
}
|
||||
|
||||
case FILTER_TITLE:
|
||||
return $localize`Title: ${rule.value}`
|
||||
|
||||
@@ -234,7 +250,8 @@ export class FilterEditorComponent
|
||||
private correspondentService: CorrespondentService,
|
||||
private documentService: DocumentService,
|
||||
private storagePathService: StoragePathService,
|
||||
public permissionsService: PermissionsService
|
||||
public permissionsService: PermissionsService,
|
||||
private customFieldService: CustomFieldsService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@@ -246,11 +263,13 @@ export class FilterEditorComponent
|
||||
correspondents: Correspondent[] = []
|
||||
documentTypes: DocumentType[] = []
|
||||
storagePaths: StoragePath[] = []
|
||||
customFields: CustomField[] = []
|
||||
|
||||
tagDocumentCounts: SelectionDataItem[]
|
||||
correspondentDocumentCounts: SelectionDataItem[]
|
||||
documentTypeDocumentCounts: SelectionDataItem[]
|
||||
storagePathDocumentCounts: SelectionDataItem[]
|
||||
customFieldDocumentCounts: SelectionDataItem[]
|
||||
|
||||
_textFilter = ''
|
||||
_moreLikeId: number
|
||||
@@ -288,6 +307,7 @@ export class FilterEditorComponent
|
||||
correspondentSelectionModel = new FilterableDropdownSelectionModel()
|
||||
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
|
||||
storagePathSelectionModel = new FilterableDropdownSelectionModel()
|
||||
customFieldSelectionModel = new FilterableDropdownSelectionModel()
|
||||
|
||||
dateCreatedBefore: string
|
||||
dateCreatedAfter: string
|
||||
@@ -322,6 +342,7 @@ export class FilterEditorComponent
|
||||
this.storagePathSelectionModel.clear(false)
|
||||
this.tagSelectionModel.clear(false)
|
||||
this.correspondentSelectionModel.clear(false)
|
||||
this.customFieldSelectionModel.clear(false)
|
||||
this._textFilter = null
|
||||
this._moreLikeId = null
|
||||
this.dateAddedBefore = null
|
||||
@@ -347,7 +368,7 @@ export class FilterEditorComponent
|
||||
this._textFilter = rule.value
|
||||
this.textFilterTarget = TEXT_FILTER_TARGET_ASN
|
||||
break
|
||||
case FILTER_CUSTOM_FIELDS:
|
||||
case FILTER_CUSTOM_FIELDS_TEXT:
|
||||
this._textFilter = rule.value
|
||||
this.textFilterTarget = TEXT_FILTER_TARGET_CUSTOM_FIELDS
|
||||
break
|
||||
@@ -488,6 +509,36 @@ export class FilterEditorComponent
|
||||
false
|
||||
)
|
||||
break
|
||||
case FILTER_HAS_CUSTOM_FIELDS_ALL:
|
||||
this.customFieldSelectionModel.logicalOperator = LogicalOperator.And
|
||||
this.customFieldSelectionModel.set(
|
||||
rule.value ? +rule.value : null,
|
||||
ToggleableItemState.Selected,
|
||||
false
|
||||
)
|
||||
break
|
||||
case FILTER_HAS_CUSTOM_FIELDS_ANY:
|
||||
this.customFieldSelectionModel.logicalOperator = LogicalOperator.Or
|
||||
this.customFieldSelectionModel.set(
|
||||
rule.value ? +rule.value : null,
|
||||
ToggleableItemState.Selected,
|
||||
false
|
||||
)
|
||||
break
|
||||
case FILTER_HAS_ANY_CUSTOM_FIELDS:
|
||||
this.customFieldSelectionModel.set(
|
||||
null,
|
||||
ToggleableItemState.Selected,
|
||||
false
|
||||
)
|
||||
break
|
||||
case FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS:
|
||||
this.customFieldSelectionModel.set(
|
||||
rule.value ? +rule.value : null,
|
||||
ToggleableItemState.Excluded,
|
||||
false
|
||||
)
|
||||
break
|
||||
case FILTER_ASN_ISNULL:
|
||||
this.textFilterTarget = TEXT_FILTER_TARGET_ASN
|
||||
this.textFilterModifier =
|
||||
@@ -595,7 +646,7 @@ export class FilterEditorComponent
|
||||
this.textFilterTarget == TEXT_FILTER_TARGET_CUSTOM_FIELDS
|
||||
) {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_CUSTOM_FIELDS,
|
||||
rule_type: FILTER_CUSTOM_FIELDS_TEXT,
|
||||
value: this._textFilter,
|
||||
})
|
||||
}
|
||||
@@ -703,6 +754,35 @@ export class FilterEditorComponent
|
||||
})
|
||||
})
|
||||
}
|
||||
if (this.customFieldSelectionModel.isNoneSelected()) {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_HAS_ANY_CUSTOM_FIELDS,
|
||||
value: 'false',
|
||||
})
|
||||
} else {
|
||||
const customFieldFilterType =
|
||||
this.customFieldSelectionModel.logicalOperator == LogicalOperator.And
|
||||
? FILTER_HAS_CUSTOM_FIELDS_ALL
|
||||
: FILTER_HAS_CUSTOM_FIELDS_ANY
|
||||
this.customFieldSelectionModel
|
||||
.getSelectedItems()
|
||||
.filter((field) => field.id)
|
||||
.forEach((field) => {
|
||||
filterRules.push({
|
||||
rule_type: customFieldFilterType,
|
||||
value: field.id?.toString(),
|
||||
})
|
||||
})
|
||||
this.customFieldSelectionModel
|
||||
.getExcludedItems()
|
||||
.filter((field) => field.id)
|
||||
.forEach((field) => {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS,
|
||||
value: field.id?.toString(),
|
||||
})
|
||||
})
|
||||
}
|
||||
if (this.dateCreatedBefore) {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_CREATED_BEFORE,
|
||||
@@ -845,6 +925,8 @@ export class FilterEditorComponent
|
||||
selectionData?.selected_correspondents ?? null
|
||||
this.storagePathDocumentCounts =
|
||||
selectionData?.selected_storage_paths ?? null
|
||||
this.customFieldDocumentCounts =
|
||||
selectionData?.selected_custom_fields ?? null
|
||||
}
|
||||
|
||||
rulesModified: boolean = false
|
||||
@@ -905,6 +987,16 @@ export class FilterEditorComponent
|
||||
.listAll()
|
||||
.subscribe((result) => (this.storagePaths = result.results))
|
||||
}
|
||||
if (
|
||||
this.permissionsService.currentUserCan(
|
||||
PermissionAction.View,
|
||||
PermissionType.CustomField
|
||||
)
|
||||
) {
|
||||
this.customFieldService
|
||||
.listAll()
|
||||
.subscribe((result) => (this.customFields = result.results))
|
||||
}
|
||||
|
||||
this.textFilterDebounce = new Subject<string>()
|
||||
|
||||
@@ -961,6 +1053,10 @@ export class FilterEditorComponent
|
||||
this.storagePathSelectionModel.apply()
|
||||
}
|
||||
|
||||
onCustomFieldsDropdownOpen() {
|
||||
this.customFieldSelectionModel.apply()
|
||||
}
|
||||
|
||||
updateTextFilter(text) {
|
||||
this._textFilter = text
|
||||
this.documentService.searchQuery = text
|
||||
|
@@ -24,7 +24,7 @@
|
||||
<ngb-pagination class="col-auto" [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" [maxSize]="5" (pageChange)="reloadData()" size="sm" aria-label="Pagination"></ngb-pagination>
|
||||
</div>
|
||||
|
||||
<div class="card border mb-3">
|
||||
<div class="card border table-responsive mb-3">
|
||||
<table class="table table-striped align-middle shadow-sm mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -74,7 +74,7 @@
|
||||
}
|
||||
<td scope="row">
|
||||
<div class="btn-group d-block d-sm-none">
|
||||
<div ngbDropdown class="d-inline-block">
|
||||
<div ngbDropdown container="body" class="d-inline-block">
|
||||
<button type="button" class="btn btn-link" id="actionsMenuMobile" (click)="$event.stopPropagation()" ngbDropdownToggle>
|
||||
<i-bs name="three-dots-vertical"></i-bs>
|
||||
</button>
|
||||
|
18
src-ui/src/app/data/auditlog-entry.ts
Normal file
18
src-ui/src/app/data/auditlog-entry.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { User } from './user'
|
||||
|
||||
export enum AuditLogAction {
|
||||
Create = 'create',
|
||||
Update = 'update',
|
||||
Delete = 'delete',
|
||||
}
|
||||
|
||||
export interface AuditLogEntry {
|
||||
id: number
|
||||
timestamp: string
|
||||
action: AuditLogAction
|
||||
changes: {
|
||||
[key: string]: string[]
|
||||
}
|
||||
remote_addr: string
|
||||
actor?: User
|
||||
}
|
@@ -7,6 +7,102 @@ import { ObjectWithPermissions } from './object-with-permissions'
|
||||
import { DocumentNote } from './document-note'
|
||||
import { CustomFieldInstance } from './custom-field-instance'
|
||||
|
||||
export enum DisplayMode {
|
||||
TABLE = 'table',
|
||||
SMALL_CARDS = 'smallCards',
|
||||
LARGE_CARDS = 'largeCards',
|
||||
}
|
||||
|
||||
export enum DisplayField {
|
||||
TITLE = 'title',
|
||||
CREATED = 'created',
|
||||
ADDED = 'added',
|
||||
TAGS = 'tag',
|
||||
CORRESPONDENT = 'correspondent',
|
||||
DOCUMENT_TYPE = 'documenttype',
|
||||
STORAGE_PATH = 'storagepath',
|
||||
CUSTOM_FIELD = 'custom_field_',
|
||||
NOTES = 'note',
|
||||
OWNER = 'owner',
|
||||
SHARED = 'shared',
|
||||
ASN = 'asn',
|
||||
}
|
||||
|
||||
export const DEFAULT_DISPLAY_FIELDS = [
|
||||
{
|
||||
id: DisplayField.TITLE,
|
||||
name: $localize`Title`,
|
||||
},
|
||||
{
|
||||
id: DisplayField.CREATED,
|
||||
name: $localize`Created`,
|
||||
},
|
||||
{
|
||||
id: DisplayField.ADDED,
|
||||
name: $localize`Added`,
|
||||
},
|
||||
{
|
||||
id: DisplayField.TAGS,
|
||||
name: $localize`Tags`,
|
||||
},
|
||||
{
|
||||
id: DisplayField.CORRESPONDENT,
|
||||
name: $localize`Correspondent`,
|
||||
},
|
||||
{
|
||||
id: DisplayField.DOCUMENT_TYPE,
|
||||
name: $localize`Document type`,
|
||||
},
|
||||
{
|
||||
id: DisplayField.STORAGE_PATH,
|
||||
name: $localize`Storage path`,
|
||||
},
|
||||
{
|
||||
id: DisplayField.NOTES,
|
||||
name: $localize`Notes`,
|
||||
},
|
||||
{
|
||||
id: DisplayField.OWNER,
|
||||
name: $localize`Owner`,
|
||||
},
|
||||
{
|
||||
id: DisplayField.SHARED,
|
||||
name: $localize`Shared`,
|
||||
},
|
||||
{
|
||||
id: DisplayField.ASN,
|
||||
name: $localize`ASN`,
|
||||
},
|
||||
]
|
||||
|
||||
export const DEFAULT_DASHBOARD_VIEW_PAGE_SIZE = 10
|
||||
|
||||
export const DEFAULT_DASHBOARD_DISPLAY_FIELDS = [
|
||||
DisplayField.CREATED,
|
||||
DisplayField.TITLE,
|
||||
DisplayField.TAGS,
|
||||
DisplayField.CORRESPONDENT,
|
||||
]
|
||||
|
||||
export const DOCUMENT_SORT_FIELDS = [
|
||||
{ field: 'archive_serial_number', name: $localize`ASN` },
|
||||
{ field: 'correspondent__name', name: $localize`Correspondent` },
|
||||
{ field: 'title', name: $localize`Title` },
|
||||
{ field: 'document_type__name', name: $localize`Document type` },
|
||||
{ field: 'created', name: $localize`Created` },
|
||||
{ field: 'added', name: $localize`Added` },
|
||||
{ field: 'modified', name: $localize`Modified` },
|
||||
{ field: 'num_notes', name: $localize`Notes` },
|
||||
{ field: 'owner', name: $localize`Owner` },
|
||||
]
|
||||
|
||||
export const DOCUMENT_SORT_FIELDS_FULLTEXT = [
|
||||
{
|
||||
field: 'score',
|
||||
name: $localize`:Score is a value returned by the full text search engine and specifies how well a result matches the given query:Search score`,
|
||||
},
|
||||
]
|
||||
|
||||
export interface SearchHit {
|
||||
score?: number
|
||||
rank?: number
|
||||
|
@@ -47,7 +47,11 @@ export const FILTER_OWNER_ISNULL = 34
|
||||
export const FILTER_OWNER_DOES_NOT_INCLUDE = 35
|
||||
export const FILTER_SHARED_BY_USER = 37
|
||||
|
||||
export const FILTER_CUSTOM_FIELDS = 36
|
||||
export const FILTER_CUSTOM_FIELDS_TEXT = 36
|
||||
export const FILTER_HAS_CUSTOM_FIELDS_ALL = 38
|
||||
export const FILTER_HAS_CUSTOM_FIELDS_ANY = 39
|
||||
export const FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS = 40
|
||||
export const FILTER_HAS_ANY_CUSTOM_FIELDS = 41
|
||||
|
||||
export const FILTER_RULE_TYPES: FilterRuleType[] = [
|
||||
{
|
||||
@@ -281,11 +285,36 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
id: FILTER_CUSTOM_FIELDS,
|
||||
id: FILTER_CUSTOM_FIELDS_TEXT,
|
||||
filtervar: 'custom_fields__icontains',
|
||||
datatype: 'string',
|
||||
multi: false,
|
||||
},
|
||||
{
|
||||
id: FILTER_HAS_CUSTOM_FIELDS_ALL,
|
||||
filtervar: 'custom_fields__id__all',
|
||||
datatype: 'number',
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
id: FILTER_HAS_CUSTOM_FIELDS_ANY,
|
||||
filtervar: 'custom_fields__id__in',
|
||||
datatype: 'number',
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
id: FILTER_DOES_NOT_HAVE_CUSTOM_FIELDS,
|
||||
filtervar: 'custom_fields__id__none',
|
||||
datatype: 'number',
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
id: FILTER_HAS_ANY_CUSTOM_FIELDS,
|
||||
filtervar: 'has_custom_fields',
|
||||
datatype: 'boolean',
|
||||
multi: false,
|
||||
default: true,
|
||||
},
|
||||
]
|
||||
|
||||
export interface FilterRuleType {
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { DisplayMode, DisplayField } from './document'
|
||||
import { FilterRule } from './filter-rule'
|
||||
import { ObjectWithPermissions } from './object-with-permissions'
|
||||
|
||||
@@ -13,4 +14,10 @@ export interface SavedView extends ObjectWithPermissions {
|
||||
sort_reverse: boolean
|
||||
|
||||
filter_rules: FilterRule[]
|
||||
|
||||
page_size?: number
|
||||
|
||||
display_mode?: DisplayMode
|
||||
|
||||
display_fields?: DisplayField[]
|
||||
}
|
||||
|
@@ -37,6 +37,7 @@ export const SETTINGS_KEYS = {
|
||||
NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD:
|
||||
'general-settings:notifications:consumer-suppress-on-dashboard',
|
||||
NOTES_ENABLED: 'general-settings:notes-enabled',
|
||||
AUDITLOG_ENABLED: 'general-settings:auditlog-enabled',
|
||||
SLIM_SIDEBAR: 'general-settings:slim-sidebar',
|
||||
UPDATE_CHECKING_ENABLED: 'general-settings:update-checking:enabled',
|
||||
UPDATE_CHECKING_BACKEND_SETTING:
|
||||
@@ -143,6 +144,11 @@ export const SETTINGS: UiSetting[] = [
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
key: SETTINGS_KEYS.AUDITLOG_ENABLED,
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
key: SETTINGS_KEYS.UPDATE_CHECKING_ENABLED,
|
||||
type: 'boolean',
|
||||
|
@@ -23,10 +23,12 @@ export class PermissionsGuard {
|
||||
state: RouterStateSnapshot
|
||||
): boolean | UrlTree {
|
||||
if (
|
||||
!this.permissionsService.currentUserCan(
|
||||
route.data.requiredPermission.action,
|
||||
route.data.requiredPermission.type
|
||||
)
|
||||
(route.data.requireAdmin && !this.permissionsService.isAdmin()) ||
|
||||
(route.data.requiredPermission &&
|
||||
!this.permissionsService.currentUserCan(
|
||||
route.data.requiredPermission.action,
|
||||
route.data.requiredPermission.type
|
||||
))
|
||||
) {
|
||||
// Check if tour is running 1 = TourState.ON
|
||||
if (this.tourService.getStatus() !== 1) {
|
||||
|
@@ -1,10 +1,7 @@
|
||||
import { TestBed } from '@angular/core/testing'
|
||||
import { CustomDatePipe } from './custom-date.pipe'
|
||||
import { SettingsService } from '../services/settings.service'
|
||||
import {
|
||||
HttpClientTestingModule,
|
||||
HttpTestingController,
|
||||
} from '@angular/common/http/testing'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { DatePipe } from '@angular/common'
|
||||
|
||||
describe('CustomDatePipe', () => {
|
||||
@@ -30,4 +27,14 @@ describe('CustomDatePipe', () => {
|
||||
)
|
||||
).toEqual('2023-05-04')
|
||||
})
|
||||
|
||||
it('should support relative date formatting', () => {
|
||||
const now = new Date()
|
||||
const notNow = new Date(now)
|
||||
notNow.setDate(now.getDate() - 1)
|
||||
expect(datePipe.transform(notNow, 'relative')).toEqual('1 day ago')
|
||||
notNow.setDate(now.getDate() - 2)
|
||||
expect(datePipe.transform(notNow, 'relative')).toEqual('2 days ago')
|
||||
expect(datePipe.transform(now, 'relative')).toEqual('Just now')
|
||||
})
|
||||
})
|
||||
|
@@ -9,6 +9,39 @@ const FORMAT_TO_ISO_FORMAT = {
|
||||
shortDate: 'y-MM-dd',
|
||||
}
|
||||
|
||||
const INTERVALS = {
|
||||
year: {
|
||||
label: $localize`%s year ago`,
|
||||
labelPlural: $localize`%s years ago`,
|
||||
interval: 31536000,
|
||||
},
|
||||
month: {
|
||||
label: $localize`%s month ago`,
|
||||
labelPlural: $localize`%s months ago`,
|
||||
interval: 2592000,
|
||||
},
|
||||
week: {
|
||||
label: $localize`%s week ago`,
|
||||
labelPlural: $localize`%s weeks ago`,
|
||||
interval: 604800,
|
||||
},
|
||||
day: {
|
||||
label: $localize`%s day ago`,
|
||||
labelPlural: $localize`%s days ago`,
|
||||
interval: 86400,
|
||||
},
|
||||
hour: {
|
||||
label: $localize`%s hour ago`,
|
||||
labelPlural: $localize`%s hours ago`,
|
||||
interval: 3600,
|
||||
},
|
||||
minute: {
|
||||
label: $localize`%s minute ago`,
|
||||
labelPlural: $localize`%s minutes ago`,
|
||||
interval: 60,
|
||||
},
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'customDate',
|
||||
})
|
||||
@@ -34,6 +67,19 @@ export class CustomDatePipe implements PipeTransform {
|
||||
this.settings.get(SETTINGS_KEYS.DATE_LOCALE) ||
|
||||
this.defaultLocale
|
||||
let f = format || this.settings.get(SETTINGS_KEYS.DATE_FORMAT)
|
||||
if (format === 'relative') {
|
||||
const seconds = Math.floor((+new Date() - +new Date(value)) / 1000)
|
||||
if (seconds < 60) return $localize`Just now`
|
||||
let counter
|
||||
for (const i in INTERVALS) {
|
||||
counter = Math.floor(seconds / INTERVALS[i].interval)
|
||||
if (counter > 0) {
|
||||
const label =
|
||||
counter > 1 ? INTERVALS[i].labelPlural : INTERVALS[i].label
|
||||
return label.replace('%s', counter.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
if (l == 'iso-8601') {
|
||||
return this.datePipe.transform(value, FORMAT_TO_ISO_FORMAT[f], timezone)
|
||||
} else {
|
||||
|
@@ -18,6 +18,8 @@ describe('ConsumerStatusService', () => {
|
||||
let httpTestingController: HttpTestingController
|
||||
let consumerStatusService: ConsumerStatusService
|
||||
let documentService: DocumentService
|
||||
let settingsService: SettingsService
|
||||
|
||||
const server = new WS(
|
||||
`${environment.webSocketProtocol}//${environment.webSocketHost}${environment.webSocketBaseUrl}status/`,
|
||||
{ jsonProtocol: true }
|
||||
@@ -25,25 +27,17 @@ describe('ConsumerStatusService', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
ConsumerStatusService,
|
||||
DocumentService,
|
||||
SettingsService,
|
||||
{
|
||||
provide: SettingsService,
|
||||
useValue: {
|
||||
currentUser: {
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
is_superuser: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
providers: [ConsumerStatusService, DocumentService, SettingsService],
|
||||
imports: [HttpClientTestingModule],
|
||||
})
|
||||
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
settingsService.currentUser = {
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
is_superuser: false,
|
||||
}
|
||||
consumerStatusService = TestBed.inject(ConsumerStatusService)
|
||||
documentService = TestBed.inject(DocumentService)
|
||||
})
|
||||
|
@@ -4,7 +4,7 @@ import { environment } from 'src/environments/environment'
|
||||
import { WebsocketConsumerStatusMessage } from '../data/websocket-consumer-status-message'
|
||||
import { SettingsService } from './settings.service'
|
||||
|
||||
// see ConsumerFilePhase in src/documents/consumer.py
|
||||
// see ProgressStatusOptions in src/documents/plugins/helpers.py
|
||||
export enum FileStatusPhase {
|
||||
STARTED = 0,
|
||||
UPLOADING = 1,
|
||||
|
@@ -19,6 +19,11 @@ import { routes } from 'src/app/app-routing.module'
|
||||
import { PermissionsGuard } from '../guards/permissions.guard'
|
||||
import { SettingsService } from './settings.service'
|
||||
import { SETTINGS_KEYS } from '../data/ui-settings'
|
||||
import {
|
||||
DisplayMode,
|
||||
DisplayField,
|
||||
DEFAULT_DISPLAY_FIELDS,
|
||||
} from '../data/document'
|
||||
|
||||
const documents = [
|
||||
{
|
||||
@@ -213,7 +218,7 @@ describe('DocumentListViewService', () => {
|
||||
documentListViewService.loadFromQueryParams(convertToParamMap(params))
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/?page=${page}&page_size=${
|
||||
documentListViewService.currentPageSize
|
||||
documentListViewService.pageSize
|
||||
}&ordering=${reverse ? '-' : ''}${sort}&truncate_content=true`
|
||||
)
|
||||
expect(req.request.method).toEqual('GET')
|
||||
@@ -231,7 +236,7 @@ describe('DocumentListViewService', () => {
|
||||
}
|
||||
documentListViewService.loadFromQueryParams(convertToParamMap(params))
|
||||
let req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.currentPageSize}&ordering=-added&truncate_content=true&tags__id__all=${tags__id__all}`
|
||||
`${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.pageSize}&ordering=-added&truncate_content=true&tags__id__all=${tags__id__all}`
|
||||
)
|
||||
expect(req.request.method).toEqual('GET')
|
||||
expect(documentListViewService.filterRules).toEqual([
|
||||
@@ -249,7 +254,7 @@ describe('DocumentListViewService', () => {
|
||||
it('should use filter rules to update query params', () => {
|
||||
documentListViewService.filterRules = filterRules
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.currentPageSize}&ordering=-created&truncate_content=true&tags__id__all=${tags__id__all}`
|
||||
`${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.pageSize}&ordering=-created&truncate_content=true&tags__id__all=${tags__id__all}`
|
||||
)
|
||||
expect(req.request.method).toEqual('GET')
|
||||
})
|
||||
@@ -257,7 +262,7 @@ describe('DocumentListViewService', () => {
|
||||
it('should support quick filter', () => {
|
||||
documentListViewService.quickFilter(filterRules)
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.currentPageSize}&ordering=-created&truncate_content=true&tags__id__all=${tags__id__all}`
|
||||
`${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.pageSize}&ordering=-created&truncate_content=true&tags__id__all=${tags__id__all}`
|
||||
)
|
||||
expect(req.request.method).toEqual('GET')
|
||||
})
|
||||
@@ -280,7 +285,7 @@ describe('DocumentListViewService', () => {
|
||||
convertToParamMap(params)
|
||||
)
|
||||
let req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/?page=${page}&page_size=${documentListViewService.currentPageSize}&ordering=-added&truncate_content=true&tags__id__all=${tags__id__all}`
|
||||
`${environment.apiBaseUrl}documents/?page=${page}&page_size=${documentListViewService.pageSize}&ordering=-added&truncate_content=true&tags__id__all=${tags__id__all}`
|
||||
)
|
||||
expect(req.request.method).toEqual('GET')
|
||||
// reset the list
|
||||
@@ -305,8 +310,7 @@ describe('DocumentListViewService', () => {
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||
)
|
||||
expect(documentListViewService.currentPage).toEqual(1)
|
||||
documentListViewService.currentPageSize = 3
|
||||
documentListViewService.reload()
|
||||
documentListViewService.pageSize = 3
|
||||
req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=3&ordering=-created&truncate_content=true`
|
||||
)
|
||||
@@ -362,7 +366,10 @@ describe('DocumentListViewService', () => {
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue(documents)
|
||||
expect(documentListViewService.currentPage).toEqual(1)
|
||||
documentListViewService.currentPageSize = 3
|
||||
documentListViewService.pageSize = 3
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=3&ordering=-created&truncate_content=true`
|
||||
)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'getLastPage')
|
||||
.mockReturnValue(Math.ceil(documents.length / 3))
|
||||
@@ -410,7 +417,13 @@ describe('DocumentListViewService', () => {
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue(documents)
|
||||
documentListViewService.currentPage = 2
|
||||
documentListViewService.currentPageSize = 3
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=2&page_size=50&ordering=-created&truncate_content=true`
|
||||
)
|
||||
documentListViewService.pageSize = 3
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=2&page_size=3&ordering=-created&truncate_content=true`
|
||||
)
|
||||
const reloadSpy = jest.spyOn(documentListViewService, 'reload')
|
||||
documentListViewService.getPrevious(1).subscribe({
|
||||
next: () => {},
|
||||
@@ -426,8 +439,7 @@ describe('DocumentListViewService', () => {
|
||||
|
||||
it('should update page size from settings', () => {
|
||||
settingsService.set(SETTINGS_KEYS.DOCUMENT_LIST_SIZE, 10)
|
||||
documentListViewService.updatePageSize()
|
||||
expect(documentListViewService.currentPageSize).toEqual(10)
|
||||
expect(documentListViewService.pageSize).toEqual(10)
|
||||
})
|
||||
|
||||
it('should support select a document', () => {
|
||||
@@ -459,8 +471,7 @@ describe('DocumentListViewService', () => {
|
||||
})
|
||||
|
||||
it('should support select page', () => {
|
||||
documentListViewService.currentPageSize = 3
|
||||
documentListViewService.reload()
|
||||
documentListViewService.pageSize = 3
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=3&ordering=-created&truncate_content=true`
|
||||
)
|
||||
@@ -544,4 +555,40 @@ describe('DocumentListViewService', () => {
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||
)
|
||||
})
|
||||
|
||||
it('should update default view state when display mode changes', () => {
|
||||
const localStorageSpy = jest.spyOn(localStorage, 'setItem')
|
||||
expect(documentListViewService.displayMode).toEqual(DisplayMode.SMALL_CARDS)
|
||||
documentListViewService.displayMode = DisplayMode.LARGE_CARDS
|
||||
expect(documentListViewService.displayMode).toEqual(DisplayMode.LARGE_CARDS)
|
||||
documentListViewService.displayMode = 'details' as any // legacy
|
||||
expect(documentListViewService.displayMode).toEqual(DisplayMode.TABLE)
|
||||
expect(localStorageSpy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should update default view state when display fields change', () => {
|
||||
const localStorageSpy = jest.spyOn(localStorage, 'setItem')
|
||||
documentListViewService.displayFields = [
|
||||
DisplayField.ADDED,
|
||||
DisplayField.TITLE,
|
||||
]
|
||||
expect(documentListViewService.displayFields).toEqual([
|
||||
DisplayField.ADDED,
|
||||
DisplayField.TITLE,
|
||||
])
|
||||
expect(localStorageSpy).toHaveBeenCalled()
|
||||
// reload triggered
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||
)
|
||||
documentListViewService.displayFields = null
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||
)
|
||||
expect(documentListViewService.displayFields).toEqual(
|
||||
DEFAULT_DISPLAY_FIELDS.filter((f) => f.id !== DisplayField.ADDED).map(
|
||||
(f) => f.id
|
||||
)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@@ -7,16 +7,17 @@ import {
|
||||
cloneFilterRules,
|
||||
isFullTextFilterRule,
|
||||
} from '../utils/filter-rules'
|
||||
import { Document } from '../data/document'
|
||||
import {
|
||||
DEFAULT_DISPLAY_FIELDS,
|
||||
DisplayField,
|
||||
DisplayMode,
|
||||
Document,
|
||||
} from '../data/document'
|
||||
import { SavedView } from '../data/saved-view'
|
||||
import { SETTINGS_KEYS } from '../data/ui-settings'
|
||||
import { DOCUMENT_LIST_SERVICE } from '../data/storage-keys'
|
||||
import { paramsFromViewState, paramsToViewState } from '../utils/query-params'
|
||||
import {
|
||||
DocumentService,
|
||||
DOCUMENT_SORT_FIELDS,
|
||||
SelectionData,
|
||||
} from './rest/document.service'
|
||||
import { DocumentService, SelectionData } from './rest/document.service'
|
||||
import { SettingsService } from './settings.service'
|
||||
|
||||
/**
|
||||
@@ -59,6 +60,21 @@ export interface ListViewState {
|
||||
* Contains the IDs of all selected documents.
|
||||
*/
|
||||
selected?: Set<number>
|
||||
|
||||
/**
|
||||
* The page size of the list view.
|
||||
*/
|
||||
pageSize?: number
|
||||
|
||||
/**
|
||||
* Display mode of the list view.
|
||||
*/
|
||||
displayMode?: DisplayMode
|
||||
|
||||
/**
|
||||
* The fields to display in the document list.
|
||||
*/
|
||||
displayFields?: DisplayField[]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,8 +96,6 @@ export class DocumentListViewService {
|
||||
|
||||
selectionData?: SelectionData
|
||||
|
||||
currentPageSize: number = this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)
|
||||
|
||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||
|
||||
private listViewStates: Map<number, ListViewState> = new Map()
|
||||
@@ -113,7 +127,7 @@ export class DocumentListViewService {
|
||||
delete savedState[k]
|
||||
}
|
||||
})
|
||||
//only use restored state attributes instead of defaults if they are not null
|
||||
// only use restored state attributes instead of defaults if they are not null
|
||||
let newState = Object.assign(this.defaultListViewState(), savedState)
|
||||
this.listViewStates.set(null, newState)
|
||||
} catch (e) {
|
||||
@@ -176,6 +190,9 @@ export class DocumentListViewService {
|
||||
if (this._activeSavedViewId) {
|
||||
this.activeListViewState.title = view.name
|
||||
}
|
||||
this.activeListViewState.displayMode = view.display_mode
|
||||
this.activeListViewState.pageSize = view.page_size
|
||||
this.activeListViewState.displayFields = view.display_fields
|
||||
|
||||
this.reduceSelectionToFilter()
|
||||
|
||||
@@ -220,7 +237,7 @@ export class DocumentListViewService {
|
||||
this.documentService
|
||||
.listFiltered(
|
||||
activeListViewState.currentPage,
|
||||
this.currentPageSize,
|
||||
activeListViewState.pageSize ?? this.pageSize,
|
||||
activeListViewState.sortField,
|
||||
activeListViewState.sortReverse,
|
||||
activeListViewState.filterRules,
|
||||
@@ -281,9 +298,8 @@ export class DocumentListViewService {
|
||||
errorMessage = Object.keys(error.error)
|
||||
.map((fieldName) => {
|
||||
const fieldError: Array<string> = error.error[fieldName]
|
||||
return `${DOCUMENT_SORT_FIELDS.find(
|
||||
(f) => f.field == fieldName
|
||||
)?.name}: ${fieldError[0]}`
|
||||
return `${this.sortFields.find((f) => f.field == fieldName)
|
||||
?.name}: ${fieldError[0]}`
|
||||
})
|
||||
.join(', ')
|
||||
} else {
|
||||
@@ -312,6 +328,14 @@ export class DocumentListViewService {
|
||||
return this.activeListViewState.filterRules
|
||||
}
|
||||
|
||||
get sortFields(): any[] {
|
||||
return this.documentService.sortFields
|
||||
}
|
||||
|
||||
get sortFieldsFullText(): any[] {
|
||||
return this.documentService.sortFieldsFullText
|
||||
}
|
||||
|
||||
set sortField(field: string) {
|
||||
this.activeListViewState.sortField = field
|
||||
this.reload()
|
||||
@@ -362,6 +386,51 @@ export class DocumentListViewService {
|
||||
this.saveDocumentListView()
|
||||
}
|
||||
|
||||
set displayMode(mode: DisplayMode) {
|
||||
this.activeListViewState.displayMode = mode
|
||||
this.saveDocumentListView()
|
||||
}
|
||||
|
||||
get displayMode(): DisplayMode {
|
||||
const mode = this.activeListViewState.displayMode ?? DisplayMode.SMALL_CARDS
|
||||
if (mode === ('details' as any)) {
|
||||
// legacy
|
||||
return DisplayMode.TABLE
|
||||
}
|
||||
return mode
|
||||
}
|
||||
|
||||
set pageSize(size: number) {
|
||||
this.activeListViewState.pageSize = size
|
||||
this.reload()
|
||||
this.saveDocumentListView()
|
||||
}
|
||||
|
||||
get pageSize(): number {
|
||||
return (
|
||||
this.activeListViewState.pageSize ??
|
||||
this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)
|
||||
)
|
||||
}
|
||||
|
||||
get displayFields(): DisplayField[] {
|
||||
let fields =
|
||||
this.activeListViewState.displayFields ??
|
||||
DEFAULT_DISPLAY_FIELDS.map((f) => f.id)
|
||||
if (!this.activeListViewState.displayFields) {
|
||||
fields = fields.filter((f) => f !== DisplayField.ADDED)
|
||||
}
|
||||
return fields.filter(
|
||||
(field) =>
|
||||
this.settings.allDisplayFields.find((f) => f.id === field) !== undefined
|
||||
)
|
||||
}
|
||||
|
||||
set displayFields(fields: DisplayField[]) {
|
||||
this.activeListViewState.displayFields = fields
|
||||
this.saveDocumentListView()
|
||||
}
|
||||
|
||||
private saveDocumentListView() {
|
||||
if (this._activeSavedViewId == null) {
|
||||
let savedState: ListViewState = {
|
||||
@@ -370,6 +439,8 @@ export class DocumentListViewService {
|
||||
filterRules: this.activeListViewState.filterRules,
|
||||
sortField: this.activeListViewState.sortField,
|
||||
sortReverse: this.activeListViewState.sortReverse,
|
||||
displayMode: this.activeListViewState.displayMode,
|
||||
displayFields: this.activeListViewState.displayFields,
|
||||
}
|
||||
localStorage.setItem(
|
||||
DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG,
|
||||
@@ -385,7 +456,7 @@ export class DocumentListViewService {
|
||||
}
|
||||
|
||||
getLastPage(): number {
|
||||
return Math.ceil(this.collectionSize / this.currentPageSize)
|
||||
return Math.ceil(this.collectionSize / this.pageSize)
|
||||
}
|
||||
|
||||
hasNext(doc: number) {
|
||||
@@ -452,13 +523,6 @@ export class DocumentListViewService {
|
||||
})
|
||||
}
|
||||
|
||||
updatePageSize() {
|
||||
let newPageSize = this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)
|
||||
if (newPageSize != this.currentPageSize) {
|
||||
this.currentPageSize = newPageSize
|
||||
}
|
||||
}
|
||||
|
||||
selectNone() {
|
||||
this.selected.clear()
|
||||
this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user