mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-02 13:45:10 -05:00
Merge branch 'dev' into feature/2396-better-mail-actions
This commit is contained in:
commit
8c5ef111d8
172
.github/scripts/cleanup-tags.py
vendored
172
.github/scripts/cleanup-tags.py
vendored
@ -7,6 +7,7 @@ import subprocess
|
|||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
from typing import Iterator
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@ -15,16 +16,17 @@ from github import ContainerPackage
|
|||||||
from github import GithubBranchApi
|
from github import GithubBranchApi
|
||||||
from github import GithubContainerRegistryApi
|
from github import GithubContainerRegistryApi
|
||||||
|
|
||||||
import docker
|
|
||||||
|
|
||||||
logger = logging.getLogger("cleanup-tags")
|
logger = logging.getLogger("cleanup-tags")
|
||||||
|
|
||||||
|
|
||||||
class DockerManifest2:
|
class ImageProperties:
|
||||||
"""
|
"""
|
||||||
Data class wrapping the Docker Image Manifest Version 2.
|
Data class wrapping the properties of an entry in the image index
|
||||||
|
manifests list. It is NOT an actual image with layers, etc
|
||||||
|
|
||||||
See https://docs.docker.com/registry/spec/manifest-v2-2/
|
https://docs.docker.com/registry/spec/manifest-v2-2/
|
||||||
|
https://github.com/opencontainers/image-spec/blob/main/manifest.md
|
||||||
|
https://github.com/opencontainers/image-spec/blob/main/descriptor.md
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, data: Dict) -> None:
|
def __init__(self, data: Dict) -> None:
|
||||||
@ -41,6 +43,45 @@ class DockerManifest2:
|
|||||||
self.platform = f"{platform_data_os}/{platform_arch}{platform_variant}"
|
self.platform = f"{platform_data_os}/{platform_arch}{platform_variant}"
|
||||||
|
|
||||||
|
|
||||||
|
class ImageIndex:
|
||||||
|
"""
|
||||||
|
Data class wrapping up logic for an OCI Image Index
|
||||||
|
JSON data. Primary use is to access the manifests listing
|
||||||
|
|
||||||
|
See https://github.com/opencontainers/image-spec/blob/main/image-index.md
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, package_url: str, tag: str) -> None:
|
||||||
|
self.qualified_name = f"{package_url}:{tag}"
|
||||||
|
logger.info(f"Getting image index for {self.qualified_name}")
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(
|
||||||
|
[
|
||||||
|
shutil.which("docker"),
|
||||||
|
"buildx",
|
||||||
|
"imagetools",
|
||||||
|
"inspect",
|
||||||
|
"--raw",
|
||||||
|
self.qualified_name,
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._data = json.loads(proc.stdout)
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
logger.error(
|
||||||
|
f"Failed to get image index for {self.qualified_name}: {e.stderr}",
|
||||||
|
)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
@property
|
||||||
|
def image_pointers(self) -> Iterator[ImageProperties]:
|
||||||
|
for manifest_data in self._data["manifests"]:
|
||||||
|
yield ImageProperties(manifest_data)
|
||||||
|
|
||||||
|
|
||||||
class RegistryTagsCleaner:
|
class RegistryTagsCleaner:
|
||||||
"""
|
"""
|
||||||
This is the base class for the image registry cleaning. Given a package
|
This is the base class for the image registry cleaning. Given a package
|
||||||
@ -87,7 +128,10 @@ class RegistryTagsCleaner:
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""
|
"""
|
||||||
This method will delete image versions, based on the selected tags to delete
|
This method will delete image versions, based on the selected tags to delete.
|
||||||
|
It behaves more like an unlinking than actual deletion. Removing the tag
|
||||||
|
simply removes a pointer to an image, but the actual image data remains accessible
|
||||||
|
if one has the sha256 digest of it.
|
||||||
"""
|
"""
|
||||||
for tag_to_delete in self.tags_to_delete:
|
for tag_to_delete in self.tags_to_delete:
|
||||||
package_version_info = self.all_pkgs_tags_to_version[tag_to_delete]
|
package_version_info = self.all_pkgs_tags_to_version[tag_to_delete]
|
||||||
@ -151,31 +195,17 @@ class RegistryTagsCleaner:
|
|||||||
|
|
||||||
# Parse manifests to locate digests pointed to
|
# Parse manifests to locate digests pointed to
|
||||||
for tag in sorted(self.tags_to_keep):
|
for tag in sorted(self.tags_to_keep):
|
||||||
full_name = f"ghcr.io/{self.repo_owner}/{self.package_name}:{tag}"
|
|
||||||
logger.info(f"Checking manifest for {full_name}")
|
|
||||||
# TODO: It would be nice to use RegistryData from docker
|
|
||||||
# except the ID doesn't map to anything in the manifest
|
|
||||||
try:
|
try:
|
||||||
proc = subprocess.run(
|
image_index = ImageIndex(
|
||||||
[
|
f"ghcr.io/{self.repo_owner}/{self.package_name}",
|
||||||
shutil.which("docker"),
|
tag,
|
||||||
"buildx",
|
|
||||||
"imagetools",
|
|
||||||
"inspect",
|
|
||||||
"--raw",
|
|
||||||
full_name,
|
|
||||||
],
|
|
||||||
capture_output=True,
|
|
||||||
)
|
)
|
||||||
|
for manifest in image_index.image_pointers:
|
||||||
manifest_list = json.loads(proc.stdout)
|
|
||||||
for manifest_data in manifest_list["manifests"]:
|
|
||||||
manifest = DockerManifest2(manifest_data)
|
|
||||||
|
|
||||||
if manifest.digest in untagged_versions:
|
if manifest.digest in untagged_versions:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Skipping deletion of {manifest.digest},"
|
f"Skipping deletion of {manifest.digest},"
|
||||||
f" referred to by {full_name}"
|
f" referred to by {image_index.qualified_name}"
|
||||||
f" for {manifest.platform}",
|
f" for {manifest.platform}",
|
||||||
)
|
)
|
||||||
del untagged_versions[manifest.digest]
|
del untagged_versions[manifest.digest]
|
||||||
@ -247,64 +277,54 @@ class RegistryTagsCleaner:
|
|||||||
# By default, keep anything which is tagged
|
# By default, keep anything which is tagged
|
||||||
self.tags_to_keep = list(set(self.all_pkgs_tags_to_version.keys()))
|
self.tags_to_keep = list(set(self.all_pkgs_tags_to_version.keys()))
|
||||||
|
|
||||||
def check_tags_pull(self):
|
def check_remaining_tags_valid(self):
|
||||||
"""
|
"""
|
||||||
This method uses the Docker Python SDK to confirm all tags which were
|
Checks the non-deleted tags are still valid. The assumption is if the
|
||||||
kept still pull, for all platforms.
|
manifest is can be inspected and each image manifest if points to can be
|
||||||
|
inspected, the image will still pull.
|
||||||
|
|
||||||
TODO: This is much slower (although more comprehensive). Maybe a Pool?
|
https://github.com/opencontainers/image-spec/blob/main/image-index.md
|
||||||
"""
|
"""
|
||||||
logger.info("Beginning confirmation step")
|
logger.info("Beginning confirmation step")
|
||||||
client = docker.from_env()
|
a_tag_failed = False
|
||||||
imgs = []
|
|
||||||
for tag in sorted(self.tags_to_keep):
|
for tag in sorted(self.tags_to_keep):
|
||||||
repository = f"ghcr.io/{self.repo_owner}/{self.package_name}"
|
|
||||||
for arch, variant in [("amd64", None), ("arm64", None), ("arm", "v7")]:
|
|
||||||
# From 11.2.0 onwards, qpdf is cross compiled, so there is a single arch, amd64
|
|
||||||
# skip others in this case
|
|
||||||
if "qpdf" in self.package_name and arch != "amd64" and tag == "11.2.0":
|
|
||||||
continue
|
|
||||||
# Skip beta and release candidate tags
|
|
||||||
elif "beta" in tag:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Build the platform name
|
try:
|
||||||
if variant is not None:
|
image_index = ImageIndex(
|
||||||
platform = f"linux/{arch}/{variant}"
|
f"ghcr.io/{self.repo_owner}/{self.package_name}",
|
||||||
else:
|
tag,
|
||||||
platform = f"linux/{arch}"
|
)
|
||||||
|
for manifest in image_index.image_pointers:
|
||||||
|
logger.info(f"Checking {manifest.digest} for {manifest.platform}")
|
||||||
|
|
||||||
try:
|
# This follows the pointer from the index to an actual image, layers and all
|
||||||
logger.info(f"Pulling {repository}:{tag} for {platform}")
|
# Note the format is @
|
||||||
image = client.images.pull(
|
digest_name = f"ghcr.io/{self.repo_owner}/{self.package_name}@{manifest.digest}"
|
||||||
repository=repository,
|
|
||||||
tag=tag,
|
|
||||||
platform=platform,
|
|
||||||
)
|
|
||||||
imgs.append(image)
|
|
||||||
except docker.errors.APIError as e:
|
|
||||||
logger.error(
|
|
||||||
f"Failed to pull {repository}:{tag}: {e}",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Prevent out of space errors by removing after a few
|
|
||||||
# pulls
|
|
||||||
if len(imgs) > 50:
|
|
||||||
for image in imgs:
|
|
||||||
try:
|
try:
|
||||||
client.images.remove(image.id)
|
|
||||||
except docker.errors.APIError as e:
|
subprocess.run(
|
||||||
err_str = str(e)
|
[
|
||||||
# Ignore attempts to remove images that are partly shared
|
shutil.which("docker"),
|
||||||
# Ignore images which are somehow gone already
|
"buildx",
|
||||||
if (
|
"imagetools",
|
||||||
"must be forced" not in err_str
|
"inspect",
|
||||||
and "No such image" not in err_str
|
"--raw",
|
||||||
):
|
digest_name,
|
||||||
logger.error(
|
],
|
||||||
f"Remove image ghcr.io/{self.repo_owner}/{self.package_name}:{tag} failed: {e}",
|
capture_output=True,
|
||||||
)
|
check=True,
|
||||||
imgs = []
|
)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
logger.error(f"Failed to inspect digest: {e.stderr}")
|
||||||
|
a_tag_failed = True
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
a_tag_failed = True
|
||||||
|
logger.error(f"Failed to inspect: {e.stderr}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if a_tag_failed:
|
||||||
|
raise Exception("At least one image tag failed to inspect")
|
||||||
|
|
||||||
|
|
||||||
class MainImageTagsCleaner(RegistryTagsCleaner):
|
class MainImageTagsCleaner(RegistryTagsCleaner):
|
||||||
@ -366,7 +386,7 @@ class MainImageTagsCleaner(RegistryTagsCleaner):
|
|||||||
|
|
||||||
class LibraryTagsCleaner(RegistryTagsCleaner):
|
class LibraryTagsCleaner(RegistryTagsCleaner):
|
||||||
"""
|
"""
|
||||||
Exists for the off change that someday, the installer library images
|
Exists for the off chance that someday, the installer library images
|
||||||
will need their own logic
|
will need their own logic
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -464,7 +484,7 @@ def _main():
|
|||||||
|
|
||||||
# Verify remaining tags still pull
|
# Verify remaining tags still pull
|
||||||
if args.is_manifest:
|
if args.is_manifest:
|
||||||
cleaner.check_tags_pull()
|
cleaner.check_remaining_tags_valid()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
5
Pipfile
5
Pipfile
@ -16,6 +16,7 @@ django-compression-middleware = "*"
|
|||||||
django-extensions = "*"
|
django-extensions = "*"
|
||||||
django-filter = "~=22.1"
|
django-filter = "~=22.1"
|
||||||
djangorestframework = "~=3.14"
|
djangorestframework = "~=3.14"
|
||||||
|
django-ipware = "*"
|
||||||
filelock = "*"
|
filelock = "*"
|
||||||
gunicorn = "*"
|
gunicorn = "*"
|
||||||
imap-tools = "*"
|
imap-tools = "*"
|
||||||
@ -61,11 +62,15 @@ bleach = "*"
|
|||||||
scipy = "==1.8.1"
|
scipy = "==1.8.1"
|
||||||
# Newer versions aren't builting yet (see https://www.piwheels.org/project/cryptography/)
|
# Newer versions aren't builting yet (see https://www.piwheels.org/project/cryptography/)
|
||||||
cryptography = "==38.0.1"
|
cryptography = "==38.0.1"
|
||||||
|
django-guardian = "*"
|
||||||
|
djangorestframework-guardian = "*"
|
||||||
|
|
||||||
# Locked version until https://github.com/django/channels_redis/issues/332
|
# Locked version until https://github.com/django/channels_redis/issues/332
|
||||||
# is resolved
|
# is resolved
|
||||||
channels-redis = "==3.4.1"
|
channels-redis = "==3.4.1"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
coveralls = "*"
|
coveralls = "*"
|
||||||
factory-boy = "*"
|
factory-boy = "*"
|
||||||
|
40
Pipfile.lock
generated
40
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "d70848276d3ac35fa361c15ac2d634344cdb08618790502669eee209fc16fa00"
|
"sha256": "0e1a26c5e9acb1d745f951f92d00d60272f83406467d90551e558972697b53cd"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {},
|
"requires": {},
|
||||||
@ -313,7 +313,7 @@
|
|||||||
"sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845",
|
"sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845",
|
||||||
"sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"
|
"sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"
|
||||||
],
|
],
|
||||||
"markers": "python_full_version >= '3.6.0'",
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==2.1.1"
|
"version": "==2.1.1"
|
||||||
},
|
},
|
||||||
"click": {
|
"click": {
|
||||||
@ -329,7 +329,7 @@
|
|||||||
"sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667",
|
"sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667",
|
||||||
"sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035"
|
"sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035"
|
||||||
],
|
],
|
||||||
"markers": "python_full_version >= '3.6.2' and python_full_version < '4.0.0'",
|
"markers": "python_version < '4' and python_full_version >= '3.6.2'",
|
||||||
"version": "==0.3.0"
|
"version": "==0.3.0"
|
||||||
},
|
},
|
||||||
"click-plugins": {
|
"click-plugins": {
|
||||||
@ -472,6 +472,22 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==22.1"
|
"version": "==22.1"
|
||||||
},
|
},
|
||||||
|
"django-guardian": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:440ca61358427e575323648b25f8384739e54c38b3d655c81d75e0cd0d61b697",
|
||||||
|
"sha256:c58a68ae76922d33e6bdc0e69af1892097838de56e93e78a8361090bcd9f89a0"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.4.0"
|
||||||
|
},
|
||||||
|
"django-ipware": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:602a58325a4808bd19197fef2676a0b2da2df40d0ecf21be414b2ff48c72ad05",
|
||||||
|
"sha256:878dbb06a87e25550798e9ef3204ed70a200dd8b15e47dcef848cf08244f04c9"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==4.0.2"
|
||||||
|
},
|
||||||
"djangorestframework": {
|
"djangorestframework": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8",
|
"sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8",
|
||||||
@ -480,6 +496,14 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.14.0"
|
"version": "==3.14.0"
|
||||||
},
|
},
|
||||||
|
"djangorestframework-guardian": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1883756452d9bfcc2a51fb4e039a6837a8f6697c756447aa83af085749b59330",
|
||||||
|
"sha256:3bd3dd6ea58e1bceca5048faf6f8b1a93bb5dcff30ba5eb91b9a0e190a48a0c7"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.3.0"
|
||||||
|
},
|
||||||
"filelock": {
|
"filelock": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de",
|
"sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de",
|
||||||
@ -2205,7 +2229,7 @@
|
|||||||
"sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845",
|
"sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845",
|
||||||
"sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"
|
"sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"
|
||||||
],
|
],
|
||||||
"markers": "python_full_version >= '3.6.0'",
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==2.1.1"
|
"version": "==2.1.1"
|
||||||
},
|
},
|
||||||
"click": {
|
"click": {
|
||||||
@ -2401,7 +2425,7 @@
|
|||||||
"sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874",
|
"sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874",
|
||||||
"sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"
|
"sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_full_version >= '3.6.0'",
|
||||||
"version": "==3.3.7"
|
"version": "==3.3.7"
|
||||||
},
|
},
|
||||||
"markupsafe": {
|
"markupsafe": {
|
||||||
@ -2455,7 +2479,7 @@
|
|||||||
"sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8",
|
"sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8",
|
||||||
"sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"
|
"sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_full_version >= '3.6.0'",
|
||||||
"version": "==1.3.4"
|
"version": "==1.3.4"
|
||||||
},
|
},
|
||||||
"mkdocs": {
|
"mkdocs": {
|
||||||
@ -2800,7 +2824,7 @@
|
|||||||
"sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb",
|
"sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb",
|
||||||
"sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"
|
"sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_full_version >= '3.6.0'",
|
||||||
"version": "==0.1"
|
"version": "==0.1"
|
||||||
},
|
},
|
||||||
"regex": {
|
"regex": {
|
||||||
@ -2987,7 +3011,7 @@
|
|||||||
"sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4",
|
"sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4",
|
||||||
"sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058"
|
"sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_full_version >= '3.6.0'",
|
||||||
"version": "==20.17.1"
|
"version": "==20.17.1"
|
||||||
},
|
},
|
||||||
"watchdog": {
|
"watchdog": {
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
version: "3.7"
|
version: "3.7"
|
||||||
services:
|
services:
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: docker.io/gotenberg/gotenberg:7.6
|
image: docker.io/gotenberg/gotenberg:7.8
|
||||||
hostname: gotenberg
|
hostname: gotenberg
|
||||||
container_name: gotenberg
|
container_name: gotenberg
|
||||||
network_mode: host
|
network_mode: host
|
||||||
|
@ -83,7 +83,7 @@ services:
|
|||||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||||
|
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: docker.io/gotenberg/gotenberg:7.6
|
image: docker.io/gotenberg/gotenberg:7.8
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||||
# want to allow external content like tracking pixels or even javascript.
|
# want to allow external content like tracking pixels or even javascript.
|
||||||
|
@ -77,7 +77,7 @@ services:
|
|||||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||||
|
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: docker.io/gotenberg/gotenberg:7.6
|
image: docker.io/gotenberg/gotenberg:7.8
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||||
|
@ -65,7 +65,7 @@ services:
|
|||||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||||
|
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: docker.io/gotenberg/gotenberg:7.6
|
image: docker.io/gotenberg/gotenberg:7.8
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||||
|
@ -414,13 +414,6 @@ structure as in the previous example above.
|
|||||||
Defining a storage path is optional. If no storage path is defined for a
|
Defining a storage path is optional. If no storage path is defined for a
|
||||||
document, the global `PAPERLESS_FILENAME_FORMAT` is applied.
|
document, the global `PAPERLESS_FILENAME_FORMAT` is applied.
|
||||||
|
|
||||||
!!! warning
|
|
||||||
|
|
||||||
If you adjust the format of an existing storage path, old documents
|
|
||||||
don't get relocated automatically. You need to run the
|
|
||||||
[document renamer](/administration#renamer) to
|
|
||||||
adjust their paths.
|
|
||||||
|
|
||||||
## Celery Monitoring {#celery-monitoring}
|
## Celery Monitoring {#celery-monitoring}
|
||||||
|
|
||||||
The monitoring tool
|
The monitoring tool
|
||||||
@ -501,3 +494,9 @@ You can also set the default for new tables (this does NOT affect
|
|||||||
existing tables) with:
|
existing tables) with:
|
||||||
|
|
||||||
`ALTER DATABASE <db_name> CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;`
|
`ALTER DATABASE <db_name> CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;`
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
Using mariadb version 10.4+ is recommended. Using the `utf8mb3` character set on
|
||||||
|
an older system may fix issues that can arise while setting up Paperless-ngx but
|
||||||
|
`utf8mb3` can cause issues with consumption (where `utf8mb4` does not).
|
||||||
|
@ -16,6 +16,8 @@ The API provides 7 main endpoints:
|
|||||||
- `/api/tags/`: Full CRUD support.
|
- `/api/tags/`: Full CRUD support.
|
||||||
- `/api/mail_accounts/`: Full CRUD support.
|
- `/api/mail_accounts/`: Full CRUD support.
|
||||||
- `/api/mail_rules/`: Full CRUD support.
|
- `/api/mail_rules/`: Full CRUD support.
|
||||||
|
- `/api/users/`: Full CRUD support.
|
||||||
|
- `/api/groups/`: Full CRUD support.
|
||||||
|
|
||||||
All of these endpoints except for the logging endpoint allow you to
|
All of these endpoints except for the logging endpoint allow you to
|
||||||
fetch, edit and delete individual objects by appending their primary key
|
fetch, edit and delete individual objects by appending their primary key
|
||||||
@ -254,6 +256,7 @@ The endpoint supports the following optional form fields:
|
|||||||
- `document_type`: Similar to correspondent.
|
- `document_type`: Similar to correspondent.
|
||||||
- `tags`: Similar to correspondent. Specify this multiple times to
|
- `tags`: Similar to correspondent. Specify this multiple times to
|
||||||
have multiple tags added to the document.
|
have multiple tags added to the document.
|
||||||
|
- `owner`: An optional user ID to set as the owner.
|
||||||
|
|
||||||
The endpoint will immediately return "OK" if the document consumption
|
The endpoint will immediately return "OK" if the document consumption
|
||||||
process was started successfully. No additional status information about
|
process was started successfully. No additional status information about
|
||||||
|
@ -1,12 +1,83 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## paperless-ngx 1.12.1
|
## paperless-ngx 1.13.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Feature: allow disable warn on close saved view with changes [@shamoon](https://github.com/shamoon) ([#2681](https://github.com/paperless-ngx/paperless-ngx/pull/2681))
|
||||||
|
- Feature: Add option to enable response compression [@stumpylog](https://github.com/stumpylog) ([#2621](https://github.com/paperless-ngx/paperless-ngx/pull/2621))
|
||||||
|
- Feature: split documents on ASN barcode [@muued](https://github.com/muued) ([#2554](https://github.com/paperless-ngx/paperless-ngx/pull/2554))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: Ignore path filtering didn't handle sub directories [@stumpylog](https://github.com/stumpylog) ([#2674](https://github.com/paperless-ngx/paperless-ngx/pull/2674))
|
||||||
|
- Bugfix: Generation of secret key hangs during install script [@stumpylog](https://github.com/stumpylog) ([#2657](https://github.com/paperless-ngx/paperless-ngx/pull/2657))
|
||||||
|
- Fix: Remove files produced by barcode splitting when completed [@stumpylog](https://github.com/stumpylog) ([#2648](https://github.com/paperless-ngx/paperless-ngx/pull/2648))
|
||||||
|
- Fix: add missing storage path placeholders [@shamoon](https://github.com/shamoon) ([#2651](https://github.com/paperless-ngx/paperless-ngx/pull/2651))
|
||||||
|
- Fix long dropdown contents break document detail column view [@shamoon](https://github.com/shamoon) ([#2638](https://github.com/paperless-ngx/paperless-ngx/pull/2638))
|
||||||
|
- Fix: tags dropdown should stay closed when removing [@shamoon](https://github.com/shamoon) ([#2625](https://github.com/paperless-ngx/paperless-ngx/pull/2625))
|
||||||
|
- Bugfix: Configure scheduled tasks to expire after some time [@stumpylog](https://github.com/stumpylog) ([#2614](https://github.com/paperless-ngx/paperless-ngx/pull/2614))
|
||||||
|
- Bugfix: Limit management list pagination maxSize to 5 [@Kaaybi](https://github.com/Kaaybi) ([#2618](https://github.com/paperless-ngx/paperless-ngx/pull/2618))
|
||||||
|
- Fix: Don't crash on bad ASNs during indexing [@stumpylog](https://github.com/stumpylog) ([#2586](https://github.com/paperless-ngx/paperless-ngx/pull/2586))
|
||||||
|
- Fix: Prevent mktime OverflowError except in even more rare caes [@stumpylog](https://github.com/stumpylog) ([#2574](https://github.com/paperless-ngx/paperless-ngx/pull/2574))
|
||||||
|
- Bugfix: Whoosh relative date queries weren't handling timezones [@stumpylog](https://github.com/stumpylog) ([#2566](https://github.com/paperless-ngx/paperless-ngx/pull/2566))
|
||||||
|
- Fix importing files with non-ascii names [@Kexogg](https://github.com/Kexogg) ([#2555](https://github.com/paperless-ngx/paperless-ngx/pull/2555))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Chore: update recommended Gotenberg to 7.8, docs note possible incompatibility [@shamoon](https://github.com/shamoon) ([#2608](https://github.com/paperless-ngx/paperless-ngx/pull/2608))
|
||||||
|
- [Documentation] Add v1.12.2 changelog [@github-actions](https://github.com/github-actions) ([#2553](https://github.com/paperless-ngx/paperless-ngx/pull/2553))
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
|
||||||
|
- Chore: Faster Docker image cleanup [@stumpylog](https://github.com/stumpylog) ([#2687](https://github.com/paperless-ngx/paperless-ngx/pull/2687))
|
||||||
|
- Chore: Remove duplicated folder [@stumpylog](https://github.com/stumpylog) ([#2561](https://github.com/paperless-ngx/paperless-ngx/pull/2561))
|
||||||
|
- Chore: Switch test coverage to Codecov [@stumpylog](https://github.com/stumpylog) ([#2582](https://github.com/paperless-ngx/paperless-ngx/pull/2582))
|
||||||
|
- Bump docker/build-push-action from 3 to 4 [@dependabot](https://github.com/dependabot) ([#2576](https://github.com/paperless-ngx/paperless-ngx/pull/2576))
|
||||||
|
- Chore: Run tests which require convert in the CI [@stumpylog](https://github.com/stumpylog) ([#2570](https://github.com/paperless-ngx/paperless-ngx/pull/2570))
|
||||||
|
|
||||||
|
- Feature: split documents on ASN barcode [@muued](https://github.com/muued) ([#2554](https://github.com/paperless-ngx/paperless-ngx/pull/2554))
|
||||||
|
- Bugfix: Whoosh relative date queries weren't handling timezones [@stumpylog](https://github.com/stumpylog) ([#2566](https://github.com/paperless-ngx/paperless-ngx/pull/2566))
|
||||||
|
- Fix importing files with non-ascii names [@Kexogg](https://github.com/Kexogg) ([#2555](https://github.com/paperless-ngx/paperless-ngx/pull/2555))
|
||||||
|
|
||||||
|
## paperless-ngx 1.12.2
|
||||||
|
|
||||||
_Note: Version 1.12.x introduced searching of comments which will work for comments added after the upgrade but a reindex of the search index is required in order to be able to search
|
_Note: Version 1.12.x introduced searching of comments which will work for comments added after the upgrade but a reindex of the search index is required in order to be able to search
|
||||||
older comments. The Docker image will automatically perform this reindex, bare metal installations will have to perform this manually, see [the docs](https://docs.paperless-ngx.com/administration/#index)._
|
older comments. The Docker image will automatically perform this reindex, bare metal installations will have to perform this manually, see [the docs](https://docs.paperless-ngx.com/administration/#index)._
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Bugfix: Allow pre-consume scripts to modify incoming file [@stumpylog](https://github.com/stumpylog) ([#2547](https://github.com/paperless-ngx/paperless-ngx/pull/2547))
|
||||||
|
- Bugfix: Return to page based barcode scanning [@stumpylog](https://github.com/stumpylog) ([#2544](https://github.com/paperless-ngx/paperless-ngx/pull/2544))
|
||||||
|
- Fix: Try to prevent title debounce overwriting [@shamoon](https://github.com/shamoon) ([#2543](https://github.com/paperless-ngx/paperless-ngx/pull/2543))
|
||||||
|
- Fix comment search highlight + multi-word search [@shamoon](https://github.com/shamoon) ([#2542](https://github.com/paperless-ngx/paperless-ngx/pull/2542))
|
||||||
|
- Bugfix: Request PDF/A format from Gotenberg [@stumpylog](https://github.com/stumpylog) ([#2530](https://github.com/paperless-ngx/paperless-ngx/pull/2530))
|
||||||
|
- Fix: Trigger reindex for pre-existing comments [@shamoon](https://github.com/shamoon) ([#2519](https://github.com/paperless-ngx/paperless-ngx/pull/2519))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Bugfix: Allow pre-consume scripts to modify incoming file [@stumpylog](https://github.com/stumpylog) ([#2547](https://github.com/paperless-ngx/paperless-ngx/pull/2547))
|
||||||
|
- Fix: Trigger reindex for pre-existing comments [@shamoon](https://github.com/shamoon) ([#2519](https://github.com/paperless-ngx/paperless-ngx/pull/2519))
|
||||||
|
- Minor updates to development documentation [@clemensrieder](https://github.com/clemensrieder) ([#2474](https://github.com/paperless-ngx/paperless-ngx/pull/2474))
|
||||||
|
- [Documentation] Add v1.12.1 changelog [@github-actions](https://github.com/github-actions) ([#2515](https://github.com/paperless-ngx/paperless-ngx/pull/2515))
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
|
||||||
|
- Chore: Fix tag cleaner to work with attestations [@stumpylog](https://github.com/stumpylog) ([#2532](https://github.com/paperless-ngx/paperless-ngx/pull/2532))
|
||||||
|
- Chore: Make installers statically versioned [@stumpylog](https://github.com/stumpylog) ([#2517](https://github.com/paperless-ngx/paperless-ngx/pull/2517))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
- Bugfix: Allow pre-consume scripts to modify incoming file [@stumpylog](https://github.com/stumpylog) ([#2547](https://github.com/paperless-ngx/paperless-ngx/pull/2547))
|
||||||
|
- Bugfix: Return to page based barcode scanning [@stumpylog](https://github.com/stumpylog) ([#2544](https://github.com/paperless-ngx/paperless-ngx/pull/2544))
|
||||||
|
- Fix: Try to prevent title debounce overwriting [@shamoon](https://github.com/shamoon) ([#2543](https://github.com/paperless-ngx/paperless-ngx/pull/2543))
|
||||||
|
- Fix comment search highlight + multi-word search [@shamoon](https://github.com/shamoon) ([#2542](https://github.com/paperless-ngx/paperless-ngx/pull/2542))
|
||||||
|
- Bugfix: Request PDF/A format from Gotenberg [@stumpylog](https://github.com/stumpylog) ([#2530](https://github.com/paperless-ngx/paperless-ngx/pull/2530))
|
||||||
|
|
||||||
|
## paperless-ngx 1.12.1
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
- Fix: comments not showing in search until after manual reindex in v1.12 [@shamoon](https://github.com/shamoon) ([#2513](https://github.com/paperless-ngx/paperless-ngx/pull/2513))
|
- Fix: comments not showing in search until after manual reindex in v1.12 [@shamoon](https://github.com/shamoon) ([#2513](https://github.com/paperless-ngx/paperless-ngx/pull/2513))
|
||||||
- Fix: date range search broken in 1.12 [@shamoon](https://github.com/shamoon) ([#2509](https://github.com/paperless-ngx/paperless-ngx/pull/2509))
|
- Fix: date range search broken in 1.12 [@shamoon](https://github.com/shamoon) ([#2509](https://github.com/paperless-ngx/paperless-ngx/pull/2509))
|
||||||
|
|
||||||
|
@ -262,6 +262,14 @@ do CORS calls. Set this to your public domain name.
|
|||||||
|
|
||||||
Defaults to "<http://localhost:8000>".
|
Defaults to "<http://localhost:8000>".
|
||||||
|
|
||||||
|
`PAPERLESS_TRUSTED_PROXIES=<comma-separated-list>`
|
||||||
|
|
||||||
|
: This may be needed to prevent IP address spoofing if you are using e.g.
|
||||||
|
fail2ban with log entries for failed authorization attempts. Value should be
|
||||||
|
IP address(es).
|
||||||
|
|
||||||
|
Defaults to empty string.
|
||||||
|
|
||||||
`PAPERLESS_FORCE_SCRIPT_NAME=<path>`
|
`PAPERLESS_FORCE_SCRIPT_NAME=<path>`
|
||||||
|
|
||||||
: To host paperless under a subpath url like example.com/paperless you
|
: To host paperless under a subpath url like example.com/paperless you
|
||||||
@ -626,7 +634,7 @@ services:
|
|||||||
# ...
|
# ...
|
||||||
|
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: gotenberg/gotenberg:7.6
|
image: gotenberg/gotenberg:7.8
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||||
# want to allow external content like tracking pixels or even javascript.
|
# want to allow external content like tracking pixels or even javascript.
|
||||||
@ -999,13 +1007,20 @@ within your documents.
|
|||||||
`PAPERLESS_CONSUMER_IGNORE_PATTERNS=<json>`
|
`PAPERLESS_CONSUMER_IGNORE_PATTERNS=<json>`
|
||||||
|
|
||||||
: By default, paperless ignores certain files and folders in the
|
: By default, paperless ignores certain files and folders in the
|
||||||
consumption directory, such as system files created by the Mac OS.
|
consumption directory, such as system files created by the Mac OS
|
||||||
|
or hidden folders some tools use to store data.
|
||||||
|
|
||||||
This can be adjusted by configuring a custom json array with
|
This can be adjusted by configuring a custom json array with
|
||||||
patterns to exclude.
|
patterns to exclude.
|
||||||
|
|
||||||
|
For example, `.DS_STORE/*` will ignore any files found in a folder
|
||||||
|
named `.DS_STORE`, including `.DS_STORE/bar.pdf` and `foo/.DS_STORE/bar.pdf`
|
||||||
|
|
||||||
|
A pattern like `._*` will ignore anything starting with `._`, including:
|
||||||
|
`._foo.pdf` and `._bar/foo.pdf`
|
||||||
|
|
||||||
Defaults to
|
Defaults to
|
||||||
`[".DS_STORE/*", "._*", ".stfolder/*", ".stversions/*", ".localized/*", "desktop.ini"]`.
|
`[".DS_STORE/*", "._*", ".stfolder/*", ".stversions/*", ".localized/*", "desktop.ini", "@eaDir/*"]`.
|
||||||
|
|
||||||
## Binaries
|
## Binaries
|
||||||
|
|
||||||
|
@ -251,7 +251,7 @@ these parts have to be translated separately.
|
|||||||
- The translated strings need to be placed in the
|
- The translated strings need to be placed in the
|
||||||
`src-ui/src/locale/` folder.
|
`src-ui/src/locale/` folder.
|
||||||
- In order to extract added or changed strings from the source files,
|
- In order to extract added or changed strings from the source files,
|
||||||
call `ng xi18n --ivy`.
|
call `ng extract-i18n`.
|
||||||
|
|
||||||
Adding new languages requires adding the translated files in the
|
Adding new languages requires adding the translated files in the
|
||||||
`src-ui/src/locale/` folder and adjusting a couple files.
|
`src-ui/src/locale/` folder and adjusting a couple files.
|
||||||
|
@ -708,6 +708,12 @@ below use PostgreSQL, but are applicable to MySQL/MariaDB with the
|
|||||||
MySQL also enforces limits on maximum lengths, but does so differently than
|
MySQL also enforces limits on maximum lengths, but does so differently than
|
||||||
PostgreSQL. It may not be possible to migrate to MySQL due to this.
|
PostgreSQL. It may not be possible to migrate to MySQL due to this.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
Using mariadb version 10.4+ is recommended. Using the `utf8mb3` character set on
|
||||||
|
an older system may fix issues that can arise while setting up Paperless-ngx but
|
||||||
|
`utf8mb3` can cause issues with consumption (where `utf8mb4` does not).
|
||||||
|
|
||||||
1. Stop paperless, if it is running.
|
1. Stop paperless, if it is running.
|
||||||
|
|
||||||
2. Tell paperless to use PostgreSQL:
|
2. Tell paperless to use PostgreSQL:
|
||||||
|
@ -202,6 +202,39 @@ configured via `PAPERLESS_EMAIL_TASK_CRON` (see [software tweaks](/configuration
|
|||||||
You can also submit a document using the REST API, see [POSTing documents](/api#file-uploads)
|
You can also submit a document using the REST API, see [POSTing documents](/api#file-uploads)
|
||||||
for details.
|
for details.
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
As of version 1.13.0 Paperless-ngx added core support for user / group permissions. Permissions is
|
||||||
|
based around an object 'owner' and 'view' and 'edit' permissions can be granted to other users
|
||||||
|
or groups.
|
||||||
|
|
||||||
|
Permissions uses the built-in user model of the backend framework, Django.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
|
||||||
|
After migration to version 1.13.0 all existing documents, tags etc. will have no explicit owner
|
||||||
|
set which means they will be visible / editable by all users. Once an object has an owner set,
|
||||||
|
only the owner can explicitly grant / revoke permissions.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
|
||||||
|
When first migrating to permissions it is recommended to user a 'superuser' account (which
|
||||||
|
would usually have been setup during installation) to ensure you have full permissions.
|
||||||
|
|
||||||
|
Note that superusers have access to all objects.
|
||||||
|
|
||||||
|
Permissions can be set using the new "Permissions" tab when editing documents, or bulk-applied
|
||||||
|
in the UI by selecting documents and choosing the "Permissions" button. Owner can also optionally
|
||||||
|
be set for documents uploaded via the API. Documents consumed via the consumption dir currently
|
||||||
|
do not have an owner set.
|
||||||
|
|
||||||
|
### Users and Groups
|
||||||
|
|
||||||
|
Paperless-ngx versions after 1.13.0 allow creating and editing users and groups via the 'frontend' UI.
|
||||||
|
These can be found under Settings > Users & Groups, assuming the user has access. If a user is designated
|
||||||
|
as a member of a group those permissions will be inherited and this is reflected in the UI. Explicit
|
||||||
|
permissions can be granted to limit access to certain parts of the UI (and corresponding API endpoints).
|
||||||
|
|
||||||
## Best practices {#basic-searching}
|
## Best practices {#basic-searching}
|
||||||
|
|
||||||
Paperless offers a couple tools that help you organize your document
|
Paperless offers a couple tools that help you organize your document
|
||||||
|
@ -321,7 +321,7 @@ fi
|
|||||||
wget "https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/docker-compose.$DOCKER_COMPOSE_VERSION.yml" -O docker-compose.yml
|
wget "https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/docker-compose.$DOCKER_COMPOSE_VERSION.yml" -O docker-compose.yml
|
||||||
wget "https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/.env" -O .env
|
wget "https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/.env" -O .env
|
||||||
|
|
||||||
SECRET_KEY=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | fold -w 64 | head -n 1)
|
SECRET_KEY=$(tr --delete --complement 'a-zA-Z0-9' < /dev/urandom 2>/dev/null | head --bytes 64)
|
||||||
|
|
||||||
DEFAULT_LANGUAGES=("deu eng fra ita spa")
|
DEFAULT_LANGUAGES=("deu eng fra ita spa")
|
||||||
|
|
||||||
|
@ -2,5 +2,5 @@
|
|||||||
|
|
||||||
docker run -p 5432:5432 -e POSTGRES_PASSWORD=password -v paperless_pgdata:/var/lib/postgresql/data -d postgres:13
|
docker run -p 5432:5432 -e POSTGRES_PASSWORD=password -v paperless_pgdata:/var/lib/postgresql/data -d postgres:13
|
||||||
docker run -d -p 6379:6379 redis:latest
|
docker run -d -p 6379:6379 redis:latest
|
||||||
docker run -p 3000:3000 -d gotenberg/gotenberg:7.6 gotenberg --chromium-disable-javascript=true --chromium-allow-list="file:///tmp/.*"
|
docker run -p 3000:3000 -d gotenberg/gotenberg:7.8 gotenberg --chromium-disable-javascript=true --chromium-allow-list="file:///tmp/.*"
|
||||||
docker run -p 9998:9998 -d ghcr.io/paperless-ngx/tika:latest
|
docker run -p 9998:9998 -d ghcr.io/paperless-ngx/tika:latest
|
||||||
|
68
src-ui/cypress/e2e/auth/auth.cy.ts
Normal file
68
src-ui/cypress/e2e/auth/auth.cy.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
describe('settings', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// also uses global fixtures from cypress/support/e2e.ts
|
||||||
|
|
||||||
|
// mock restricted permissions
|
||||||
|
cy.intercept('http://localhost:8000/api/ui_settings/', {
|
||||||
|
fixture: 'ui_settings/settings_restricted.json',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not allow user to edit settings', () => {
|
||||||
|
cy.visit('/dashboard')
|
||||||
|
cy.contains('Settings').should('not.exist')
|
||||||
|
cy.visit('/settings').wait(2000)
|
||||||
|
cy.contains("You don't have permissions to do that").should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not allow user to view documents', () => {
|
||||||
|
cy.visit('/dashboard')
|
||||||
|
cy.contains('Documents').should('not.exist')
|
||||||
|
cy.visit('/documents').wait(2000)
|
||||||
|
cy.contains("You don't have permissions to do that").should('exist')
|
||||||
|
cy.visit('/documents/1').wait(2000)
|
||||||
|
cy.contains("You don't have permissions to do that").should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not allow user to view correspondents', () => {
|
||||||
|
cy.visit('/dashboard')
|
||||||
|
cy.contains('Correspondents').should('not.exist')
|
||||||
|
cy.visit('/correspondents').wait(2000)
|
||||||
|
cy.contains("You don't have permissions to do that").should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not allow user to view tags', () => {
|
||||||
|
cy.visit('/dashboard')
|
||||||
|
cy.contains('Tags').should('not.exist')
|
||||||
|
cy.visit('/tags').wait(2000)
|
||||||
|
cy.contains("You don't have permissions to do that").should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not allow user to view document types', () => {
|
||||||
|
cy.visit('/dashboard')
|
||||||
|
cy.contains('Document Types').should('not.exist')
|
||||||
|
cy.visit('/documenttypes').wait(2000)
|
||||||
|
cy.contains("You don't have permissions to do that").should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not allow user to view storage paths', () => {
|
||||||
|
cy.visit('/dashboard')
|
||||||
|
cy.contains('Storage Paths').should('not.exist')
|
||||||
|
cy.visit('/storagepaths').wait(2000)
|
||||||
|
cy.contains("You don't have permissions to do that").should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not allow user to view logs', () => {
|
||||||
|
cy.visit('/dashboard')
|
||||||
|
cy.contains('Logs').should('not.exist')
|
||||||
|
cy.visit('/logs').wait(2000)
|
||||||
|
cy.contains("You don't have permissions to do that").should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not allow user to view tasks', () => {
|
||||||
|
cy.visit('/dashboard')
|
||||||
|
cy.contains('Tasks').should('not.exist')
|
||||||
|
cy.visit('/tasks').wait(2000)
|
||||||
|
cy.contains("You don't have permissions to do that").should('exist')
|
||||||
|
})
|
||||||
|
})
|
@ -6,8 +6,8 @@
|
|||||||
"user": {
|
"user": {
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"username": "user2",
|
"username": "user2",
|
||||||
"firstname": "",
|
"first_name": "",
|
||||||
"lastname": ""
|
"last_name": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -17,8 +17,8 @@
|
|||||||
"user": {
|
"user": {
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"username": "user1",
|
"username": "user1",
|
||||||
"firstname": "",
|
"first_name": "",
|
||||||
"lastname": ""
|
"last_name": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -28,8 +28,8 @@
|
|||||||
"user": {
|
"user": {
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"username": "user33",
|
"username": "user33",
|
||||||
"firstname": "",
|
"first_name": "",
|
||||||
"lastname": ""
|
"last_name": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -39,8 +39,8 @@
|
|||||||
"user": {
|
"user": {
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"username": "admin",
|
"username": "admin",
|
||||||
"firstname": "",
|
"first_name": "",
|
||||||
"lastname": ""
|
"last_name": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -14,11 +14,14 @@
|
|||||||
4
|
4
|
||||||
],
|
],
|
||||||
"created": "2022-03-22T07:24:18Z",
|
"created": "2022-03-22T07:24:18Z",
|
||||||
|
"created_date": "2022-03-22",
|
||||||
"modified": "2022-03-22T07:24:23.264859Z",
|
"modified": "2022-03-22T07:24:23.264859Z",
|
||||||
"added": "2022-03-22T07:24:22.922631Z",
|
"added": "2022-03-22T07:24:22.922631Z",
|
||||||
"archive_serial_number": null,
|
"archive_serial_number": null,
|
||||||
"original_file_name": "2022-03-22 no latin title.pdf",
|
"original_file_name": "2022-03-22 no latin title.pdf",
|
||||||
"archived_file_name": "2022-03-22 no latin title.pdf"
|
"archived_file_name": "2022-03-22 no latin title.pdf",
|
||||||
|
"owner": null,
|
||||||
|
"permissions": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
@ -29,11 +32,14 @@
|
|||||||
"content": "Test document PDF",
|
"content": "Test document PDF",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"created": "2022-03-23T07:24:18Z",
|
"created": "2022-03-23T07:24:18Z",
|
||||||
|
"created_date": "2022-03-23",
|
||||||
"modified": "2022-03-23T07:24:23.264859Z",
|
"modified": "2022-03-23T07:24:23.264859Z",
|
||||||
"added": "2022-03-23T07:24:22.922631Z",
|
"added": "2022-03-23T07:24:22.922631Z",
|
||||||
"archive_serial_number": 12345,
|
"archive_serial_number": 12345,
|
||||||
"original_file_name": "2022-03-23 lorem ipsum dolor sit amet.pdf",
|
"original_file_name": "2022-03-23 lorem ipsum dolor sit amet.pdf",
|
||||||
"archived_file_name": "2022-03-23 llorem ipsum dolor sit amet.pdf"
|
"archived_file_name": "2022-03-23 llorem ipsum dolor sit amet.pdf",
|
||||||
|
"owner": null,
|
||||||
|
"permissions": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
@ -46,11 +52,14 @@
|
|||||||
2
|
2
|
||||||
],
|
],
|
||||||
"created": "2022-03-24T07:24:18Z",
|
"created": "2022-03-24T07:24:18Z",
|
||||||
|
"created_date": "2022-03-24",
|
||||||
"modified": "2022-03-24T07:24:23.264859Z",
|
"modified": "2022-03-24T07:24:23.264859Z",
|
||||||
"added": "2022-03-24T07:24:22.922631Z",
|
"added": "2022-03-24T07:24:22.922631Z",
|
||||||
"archive_serial_number": null,
|
"archive_serial_number": null,
|
||||||
"original_file_name": "2022-03-24 dolor.pdf",
|
"original_file_name": "2022-03-24 dolor.pdf",
|
||||||
"archived_file_name": "2022-03-24 dolor.pdf"
|
"archived_file_name": "2022-03-24 dolor.pdf",
|
||||||
|
"owner": null,
|
||||||
|
"permissions": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
"id": 4,
|
||||||
@ -63,11 +72,14 @@
|
|||||||
4, 5
|
4, 5
|
||||||
],
|
],
|
||||||
"created": "2022-06-01T07:24:18Z",
|
"created": "2022-06-01T07:24:18Z",
|
||||||
|
"created_date": "2022-06-01",
|
||||||
"modified": "2022-06-01T07:24:23.264859Z",
|
"modified": "2022-06-01T07:24:23.264859Z",
|
||||||
"added": "2022-06-01T07:24:22.922631Z",
|
"added": "2022-06-01T07:24:22.922631Z",
|
||||||
"archive_serial_number": 12347,
|
"archive_serial_number": 12347,
|
||||||
"original_file_name": "2022-06-01 sit amet.pdf",
|
"original_file_name": "2022-06-01 sit amet.pdf",
|
||||||
"archived_file_name": "2022-06-01 sit amet.pdf"
|
"archived_file_name": "2022-06-01 sit amet.pdf",
|
||||||
|
"owner": null,
|
||||||
|
"permissions": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
119
src-ui/cypress/fixtures/groups/groups.json
Normal file
119
src-ui/cypress/fixtures/groups/groups.json
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
{
|
||||||
|
"count": 2,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"name": "Another Group",
|
||||||
|
"permissions": [
|
||||||
|
"add_user",
|
||||||
|
"change_user",
|
||||||
|
"delete_user",
|
||||||
|
"view_user",
|
||||||
|
"add_comment",
|
||||||
|
"change_comment",
|
||||||
|
"delete_comment",
|
||||||
|
"view_comment"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "First Group",
|
||||||
|
"permissions": [
|
||||||
|
"add_group",
|
||||||
|
"change_group",
|
||||||
|
"delete_group",
|
||||||
|
"view_group",
|
||||||
|
"add_permission",
|
||||||
|
"change_permission",
|
||||||
|
"delete_permission",
|
||||||
|
"view_permission",
|
||||||
|
"add_token",
|
||||||
|
"change_token",
|
||||||
|
"delete_token",
|
||||||
|
"view_token",
|
||||||
|
"add_tokenproxy",
|
||||||
|
"change_tokenproxy",
|
||||||
|
"delete_tokenproxy",
|
||||||
|
"view_tokenproxy",
|
||||||
|
"add_contenttype",
|
||||||
|
"change_contenttype",
|
||||||
|
"delete_contenttype",
|
||||||
|
"view_contenttype",
|
||||||
|
"add_chordcounter",
|
||||||
|
"change_chordcounter",
|
||||||
|
"delete_chordcounter",
|
||||||
|
"view_chordcounter",
|
||||||
|
"add_groupresult",
|
||||||
|
"change_groupresult",
|
||||||
|
"delete_groupresult",
|
||||||
|
"view_groupresult",
|
||||||
|
"add_taskresult",
|
||||||
|
"change_taskresult",
|
||||||
|
"delete_taskresult",
|
||||||
|
"view_taskresult",
|
||||||
|
"add_failure",
|
||||||
|
"change_failure",
|
||||||
|
"delete_failure",
|
||||||
|
"view_failure",
|
||||||
|
"add_ormq",
|
||||||
|
"change_ormq",
|
||||||
|
"delete_ormq",
|
||||||
|
"view_ormq",
|
||||||
|
"add_schedule",
|
||||||
|
"change_schedule",
|
||||||
|
"delete_schedule",
|
||||||
|
"view_schedule",
|
||||||
|
"add_success",
|
||||||
|
"change_success",
|
||||||
|
"delete_success",
|
||||||
|
"view_success",
|
||||||
|
"add_task",
|
||||||
|
"change_task",
|
||||||
|
"delete_task",
|
||||||
|
"view_task",
|
||||||
|
"add_comment",
|
||||||
|
"change_comment",
|
||||||
|
"delete_comment",
|
||||||
|
"view_comment",
|
||||||
|
"add_correspondent",
|
||||||
|
"change_correspondent",
|
||||||
|
"delete_correspondent",
|
||||||
|
"view_correspondent",
|
||||||
|
"add_document",
|
||||||
|
"change_document",
|
||||||
|
"delete_document",
|
||||||
|
"view_document",
|
||||||
|
"add_documenttype",
|
||||||
|
"change_documenttype",
|
||||||
|
"delete_documenttype",
|
||||||
|
"view_documenttype",
|
||||||
|
"add_frontendsettings",
|
||||||
|
"change_frontendsettings",
|
||||||
|
"delete_frontendsettings",
|
||||||
|
"view_frontendsettings",
|
||||||
|
"add_log",
|
||||||
|
"change_log",
|
||||||
|
"delete_log",
|
||||||
|
"view_log",
|
||||||
|
"add_savedview",
|
||||||
|
"change_savedview",
|
||||||
|
"delete_savedview",
|
||||||
|
"view_savedview",
|
||||||
|
"add_savedviewfilterrule",
|
||||||
|
"change_savedviewfilterrule",
|
||||||
|
"delete_savedviewfilterrule",
|
||||||
|
"view_savedviewfilterrule",
|
||||||
|
"add_taskattributes",
|
||||||
|
"change_taskattributes",
|
||||||
|
"delete_taskattributes",
|
||||||
|
"view_taskattributes",
|
||||||
|
"add_session",
|
||||||
|
"change_session",
|
||||||
|
"delete_session",
|
||||||
|
"view_session"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"user_id": 1,
|
"user_id": 1,
|
||||||
"username": "admin",
|
"username": "admin",
|
||||||
"display_name": "Admin",
|
|
||||||
"settings": {
|
"settings": {
|
||||||
"language": "",
|
"language": "",
|
||||||
"bulk_edit": {
|
"bulk_edit": {
|
||||||
@ -30,5 +29,131 @@
|
|||||||
"consumer_failed": true,
|
"consumer_failed": true,
|
||||||
"consumer_suppress_on_dashboard": true
|
"consumer_suppress_on_dashboard": true
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"permissions": [
|
||||||
|
"add_logentry",
|
||||||
|
"change_logentry",
|
||||||
|
"delete_logentry",
|
||||||
|
"view_logentry",
|
||||||
|
"add_group",
|
||||||
|
"change_group",
|
||||||
|
"delete_group",
|
||||||
|
"view_group",
|
||||||
|
"add_permission",
|
||||||
|
"change_permission",
|
||||||
|
"delete_permission",
|
||||||
|
"view_permission",
|
||||||
|
"add_user",
|
||||||
|
"change_user",
|
||||||
|
"delete_user",
|
||||||
|
"view_user",
|
||||||
|
"add_token",
|
||||||
|
"change_token",
|
||||||
|
"delete_token",
|
||||||
|
"view_token",
|
||||||
|
"add_tokenproxy",
|
||||||
|
"change_tokenproxy",
|
||||||
|
"delete_tokenproxy",
|
||||||
|
"view_tokenproxy",
|
||||||
|
"add_contenttype",
|
||||||
|
"change_contenttype",
|
||||||
|
"delete_contenttype",
|
||||||
|
"view_contenttype",
|
||||||
|
"add_chordcounter",
|
||||||
|
"change_chordcounter",
|
||||||
|
"delete_chordcounter",
|
||||||
|
"view_chordcounter",
|
||||||
|
"add_groupresult",
|
||||||
|
"change_groupresult",
|
||||||
|
"delete_groupresult",
|
||||||
|
"view_groupresult",
|
||||||
|
"add_taskresult",
|
||||||
|
"change_taskresult",
|
||||||
|
"delete_taskresult",
|
||||||
|
"view_taskresult",
|
||||||
|
"add_failure",
|
||||||
|
"change_failure",
|
||||||
|
"delete_failure",
|
||||||
|
"view_failure",
|
||||||
|
"add_ormq",
|
||||||
|
"change_ormq",
|
||||||
|
"delete_ormq",
|
||||||
|
"view_ormq",
|
||||||
|
"add_schedule",
|
||||||
|
"change_schedule",
|
||||||
|
"delete_schedule",
|
||||||
|
"view_schedule",
|
||||||
|
"add_success",
|
||||||
|
"change_success",
|
||||||
|
"delete_success",
|
||||||
|
"view_success",
|
||||||
|
"add_task",
|
||||||
|
"change_task",
|
||||||
|
"delete_task",
|
||||||
|
"view_task",
|
||||||
|
"add_comment",
|
||||||
|
"change_comment",
|
||||||
|
"delete_comment",
|
||||||
|
"view_comment",
|
||||||
|
"add_correspondent",
|
||||||
|
"change_correspondent",
|
||||||
|
"delete_correspondent",
|
||||||
|
"view_correspondent",
|
||||||
|
"add_document",
|
||||||
|
"change_document",
|
||||||
|
"delete_document",
|
||||||
|
"view_document",
|
||||||
|
"add_documenttype",
|
||||||
|
"change_documenttype",
|
||||||
|
"delete_documenttype",
|
||||||
|
"view_documenttype",
|
||||||
|
"add_frontendsettings",
|
||||||
|
"change_frontendsettings",
|
||||||
|
"delete_frontendsettings",
|
||||||
|
"view_frontendsettings",
|
||||||
|
"add_log",
|
||||||
|
"change_log",
|
||||||
|
"delete_log",
|
||||||
|
"view_log",
|
||||||
|
"add_paperlesstask",
|
||||||
|
"change_paperlesstask",
|
||||||
|
"delete_paperlesstask",
|
||||||
|
"view_paperlesstask",
|
||||||
|
"add_savedview",
|
||||||
|
"change_savedview",
|
||||||
|
"delete_savedview",
|
||||||
|
"view_savedview",
|
||||||
|
"add_savedviewfilterrule",
|
||||||
|
"change_savedviewfilterrule",
|
||||||
|
"delete_savedviewfilterrule",
|
||||||
|
"view_savedviewfilterrule",
|
||||||
|
"add_storagepath",
|
||||||
|
"change_storagepath",
|
||||||
|
"delete_storagepath",
|
||||||
|
"view_storagepath",
|
||||||
|
"add_tag",
|
||||||
|
"change_tag",
|
||||||
|
"delete_tag",
|
||||||
|
"view_tag",
|
||||||
|
"add_taskattributes",
|
||||||
|
"change_taskattributes",
|
||||||
|
"delete_taskattributes",
|
||||||
|
"view_taskattributes",
|
||||||
|
"add_uisettings",
|
||||||
|
"change_uisettings",
|
||||||
|
"delete_uisettings",
|
||||||
|
"view_uisettings",
|
||||||
|
"add_mailaccount",
|
||||||
|
"change_mailaccount",
|
||||||
|
"delete_mailaccount",
|
||||||
|
"view_mailaccount",
|
||||||
|
"add_mailrule",
|
||||||
|
"change_mailrule",
|
||||||
|
"delete_mailrule",
|
||||||
|
"view_mailrule",
|
||||||
|
"add_session",
|
||||||
|
"change_session",
|
||||||
|
"delete_session",
|
||||||
|
"view_session"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
84
src-ui/cypress/fixtures/ui_settings/settings_restricted.json
Normal file
84
src-ui/cypress/fixtures/ui_settings/settings_restricted.json
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
{
|
||||||
|
"user_id": 1,
|
||||||
|
"username": "admin",
|
||||||
|
"settings": {
|
||||||
|
"language": "",
|
||||||
|
"bulk_edit": {
|
||||||
|
"confirmation_dialogs": true,
|
||||||
|
"apply_on_close": false
|
||||||
|
},
|
||||||
|
"documentListSize": 50,
|
||||||
|
"dark_mode": {
|
||||||
|
"use_system": true,
|
||||||
|
"enabled": "false",
|
||||||
|
"thumb_inverted": "true"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"color": "#b198e5"
|
||||||
|
},
|
||||||
|
"document_details": {
|
||||||
|
"native_pdf_viewer": false
|
||||||
|
},
|
||||||
|
"date_display": {
|
||||||
|
"date_locale": "",
|
||||||
|
"date_format": "mediumDate"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"consumer_new_documents": true,
|
||||||
|
"consumer_success": true,
|
||||||
|
"consumer_failed": true,
|
||||||
|
"consumer_suppress_on_dashboard": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"permissions": [
|
||||||
|
"add_token",
|
||||||
|
"change_token",
|
||||||
|
"delete_token",
|
||||||
|
"view_token",
|
||||||
|
"add_tokenproxy",
|
||||||
|
"change_tokenproxy",
|
||||||
|
"delete_tokenproxy",
|
||||||
|
"view_tokenproxy",
|
||||||
|
"add_contenttype",
|
||||||
|
"change_contenttype",
|
||||||
|
"delete_contenttype",
|
||||||
|
"view_contenttype",
|
||||||
|
"add_chordcounter",
|
||||||
|
"change_chordcounter",
|
||||||
|
"delete_chordcounter",
|
||||||
|
"view_chordcounter",
|
||||||
|
"add_groupresult",
|
||||||
|
"change_groupresult",
|
||||||
|
"delete_groupresult",
|
||||||
|
"view_groupresult",
|
||||||
|
"add_failure",
|
||||||
|
"change_failure",
|
||||||
|
"delete_failure",
|
||||||
|
"view_failure",
|
||||||
|
"add_ormq",
|
||||||
|
"change_ormq",
|
||||||
|
"delete_ormq",
|
||||||
|
"view_ormq",
|
||||||
|
"add_schedule",
|
||||||
|
"change_schedule",
|
||||||
|
"delete_schedule",
|
||||||
|
"view_schedule",
|
||||||
|
"add_success",
|
||||||
|
"change_success",
|
||||||
|
"delete_success",
|
||||||
|
"view_success",
|
||||||
|
"add_task",
|
||||||
|
"change_task",
|
||||||
|
"delete_task",
|
||||||
|
"view_task",
|
||||||
|
"add_comment",
|
||||||
|
"add_frontendsettings",
|
||||||
|
"change_frontendsettings",
|
||||||
|
"delete_frontendsettings",
|
||||||
|
"view_frontendsettings",
|
||||||
|
"add_session",
|
||||||
|
"change_session",
|
||||||
|
"delete_session",
|
||||||
|
"view_session"
|
||||||
|
]
|
||||||
|
}
|
459
src-ui/cypress/fixtures/users/users.json
Normal file
459
src-ui/cypress/fixtures/users/users.json
Normal file
@ -0,0 +1,459 @@
|
|||||||
|
{
|
||||||
|
"count": 4,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"username": "admin",
|
||||||
|
"password": "**********",
|
||||||
|
"first_name": "",
|
||||||
|
"last_name": "",
|
||||||
|
"date_joined": "2022-02-14T23:11:09.103293Z",
|
||||||
|
"is_staff": true,
|
||||||
|
"is_active": true,
|
||||||
|
"is_superuser": true,
|
||||||
|
"groups": [],
|
||||||
|
"user_permissions": [],
|
||||||
|
"inherited_permissions": [
|
||||||
|
"auth.delete_permission",
|
||||||
|
"paperless_mail.change_mailrule",
|
||||||
|
"django_celery_results.add_taskresult",
|
||||||
|
"documents.view_taskattributes",
|
||||||
|
"documents.view_paperlesstask",
|
||||||
|
"django_q.add_success",
|
||||||
|
"documents.view_uisettings",
|
||||||
|
"auth.change_user",
|
||||||
|
"admin.delete_logentry",
|
||||||
|
"django_celery_results.change_taskresult",
|
||||||
|
"django_q.change_schedule",
|
||||||
|
"django_celery_results.delete_taskresult",
|
||||||
|
"paperless_mail.add_mailaccount",
|
||||||
|
"auth.change_group",
|
||||||
|
"documents.add_comment",
|
||||||
|
"paperless_mail.delete_mailaccount",
|
||||||
|
"authtoken.delete_tokenproxy",
|
||||||
|
"guardian.delete_groupobjectpermission",
|
||||||
|
"contenttypes.delete_contenttype",
|
||||||
|
"documents.change_correspondent",
|
||||||
|
"authtoken.delete_token",
|
||||||
|
"documents.delete_documenttype",
|
||||||
|
"django_q.change_ormq",
|
||||||
|
"documents.change_savedviewfilterrule",
|
||||||
|
"auth.delete_group",
|
||||||
|
"documents.add_documenttype",
|
||||||
|
"django_q.change_success",
|
||||||
|
"documents.delete_tag",
|
||||||
|
"documents.change_comment",
|
||||||
|
"django_q.delete_task",
|
||||||
|
"documents.add_savedviewfilterrule",
|
||||||
|
"django_q.view_task",
|
||||||
|
"paperless_mail.add_mailrule",
|
||||||
|
"paperless_mail.view_mailaccount",
|
||||||
|
"documents.add_frontendsettings",
|
||||||
|
"sessions.change_session",
|
||||||
|
"documents.view_savedview",
|
||||||
|
"authtoken.add_tokenproxy",
|
||||||
|
"documents.change_tag",
|
||||||
|
"documents.view_document",
|
||||||
|
"documents.add_savedview",
|
||||||
|
"auth.delete_user",
|
||||||
|
"documents.view_log",
|
||||||
|
"documents.view_comment",
|
||||||
|
"guardian.change_groupobjectpermission",
|
||||||
|
"sessions.delete_session",
|
||||||
|
"django_q.change_failure",
|
||||||
|
"guardian.change_userobjectpermission",
|
||||||
|
"documents.change_storagepath",
|
||||||
|
"documents.delete_document",
|
||||||
|
"documents.delete_taskattributes",
|
||||||
|
"django_celery_results.change_groupresult",
|
||||||
|
"django_q.add_ormq",
|
||||||
|
"guardian.view_groupobjectpermission",
|
||||||
|
"admin.change_logentry",
|
||||||
|
"django_q.delete_schedule",
|
||||||
|
"documents.delete_paperlesstask",
|
||||||
|
"django_q.view_ormq",
|
||||||
|
"documents.change_paperlesstask",
|
||||||
|
"guardian.delete_userobjectpermission",
|
||||||
|
"auth.view_permission",
|
||||||
|
"auth.view_user",
|
||||||
|
"django_q.add_schedule",
|
||||||
|
"authtoken.change_token",
|
||||||
|
"guardian.add_groupobjectpermission",
|
||||||
|
"documents.view_documenttype",
|
||||||
|
"documents.change_log",
|
||||||
|
"paperless_mail.delete_mailrule",
|
||||||
|
"auth.view_group",
|
||||||
|
"authtoken.view_token",
|
||||||
|
"admin.view_logentry",
|
||||||
|
"django_celery_results.view_chordcounter",
|
||||||
|
"django_celery_results.view_groupresult",
|
||||||
|
"documents.view_storagepath",
|
||||||
|
"documents.add_storagepath",
|
||||||
|
"django_celery_results.add_groupresult",
|
||||||
|
"documents.view_tag",
|
||||||
|
"guardian.view_userobjectpermission",
|
||||||
|
"documents.delete_correspondent",
|
||||||
|
"documents.add_tag",
|
||||||
|
"documents.delete_savedviewfilterrule",
|
||||||
|
"documents.add_correspondent",
|
||||||
|
"authtoken.view_tokenproxy",
|
||||||
|
"documents.delete_frontendsettings",
|
||||||
|
"django_celery_results.delete_chordcounter",
|
||||||
|
"django_q.change_task",
|
||||||
|
"documents.add_taskattributes",
|
||||||
|
"documents.delete_storagepath",
|
||||||
|
"sessions.add_session",
|
||||||
|
"documents.add_uisettings",
|
||||||
|
"documents.change_taskattributes",
|
||||||
|
"documents.delete_uisettings",
|
||||||
|
"django_q.delete_ormq",
|
||||||
|
"auth.change_permission",
|
||||||
|
"documents.view_savedviewfilterrule",
|
||||||
|
"documents.change_frontendsettings",
|
||||||
|
"documents.change_documenttype",
|
||||||
|
"documents.view_correspondent",
|
||||||
|
"auth.add_user",
|
||||||
|
"paperless_mail.change_mailaccount",
|
||||||
|
"documents.add_paperlesstask",
|
||||||
|
"django_q.view_success",
|
||||||
|
"django_celery_results.delete_groupresult",
|
||||||
|
"documents.delete_savedview",
|
||||||
|
"authtoken.change_tokenproxy",
|
||||||
|
"documents.view_frontendsettings",
|
||||||
|
"authtoken.add_token",
|
||||||
|
"django_celery_results.add_chordcounter",
|
||||||
|
"contenttypes.change_contenttype",
|
||||||
|
"admin.add_logentry",
|
||||||
|
"django_q.delete_failure",
|
||||||
|
"documents.change_uisettings",
|
||||||
|
"django_q.view_failure",
|
||||||
|
"documents.add_log",
|
||||||
|
"documents.change_savedview",
|
||||||
|
"paperless_mail.view_mailrule",
|
||||||
|
"django_q.view_schedule",
|
||||||
|
"documents.change_document",
|
||||||
|
"django_celery_results.change_chordcounter",
|
||||||
|
"documents.add_document",
|
||||||
|
"django_celery_results.view_taskresult",
|
||||||
|
"contenttypes.add_contenttype",
|
||||||
|
"django_q.delete_success",
|
||||||
|
"documents.delete_comment",
|
||||||
|
"django_q.add_failure",
|
||||||
|
"guardian.add_userobjectpermission",
|
||||||
|
"sessions.view_session",
|
||||||
|
"contenttypes.view_contenttype",
|
||||||
|
"auth.add_permission",
|
||||||
|
"documents.delete_log",
|
||||||
|
"django_q.add_task",
|
||||||
|
"auth.add_group"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 15,
|
||||||
|
"username": "test",
|
||||||
|
"password": "**********",
|
||||||
|
"first_name": "",
|
||||||
|
"last_name": "",
|
||||||
|
"date_joined": "2022-11-23T08:30:54Z",
|
||||||
|
"is_staff": true,
|
||||||
|
"is_active": true,
|
||||||
|
"is_superuser": false,
|
||||||
|
"groups": [
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"user_permissions": [
|
||||||
|
"add_group",
|
||||||
|
"change_group",
|
||||||
|
"delete_group",
|
||||||
|
"view_group",
|
||||||
|
"add_permission",
|
||||||
|
"change_permission",
|
||||||
|
"delete_permission",
|
||||||
|
"view_permission",
|
||||||
|
"add_token",
|
||||||
|
"change_token",
|
||||||
|
"delete_token",
|
||||||
|
"view_token",
|
||||||
|
"add_tokenproxy",
|
||||||
|
"change_tokenproxy",
|
||||||
|
"delete_tokenproxy",
|
||||||
|
"view_tokenproxy",
|
||||||
|
"add_contenttype",
|
||||||
|
"change_contenttype",
|
||||||
|
"delete_contenttype",
|
||||||
|
"view_contenttype",
|
||||||
|
"add_chordcounter",
|
||||||
|
"change_chordcounter",
|
||||||
|
"delete_chordcounter",
|
||||||
|
"view_chordcounter",
|
||||||
|
"add_groupresult",
|
||||||
|
"change_groupresult",
|
||||||
|
"delete_groupresult",
|
||||||
|
"view_groupresult",
|
||||||
|
"add_taskresult",
|
||||||
|
"change_taskresult",
|
||||||
|
"delete_taskresult",
|
||||||
|
"view_taskresult",
|
||||||
|
"add_failure",
|
||||||
|
"change_failure",
|
||||||
|
"delete_failure",
|
||||||
|
"view_failure",
|
||||||
|
"add_ormq",
|
||||||
|
"change_ormq",
|
||||||
|
"delete_ormq",
|
||||||
|
"view_ormq",
|
||||||
|
"add_schedule",
|
||||||
|
"change_schedule",
|
||||||
|
"delete_schedule",
|
||||||
|
"view_schedule",
|
||||||
|
"add_success",
|
||||||
|
"change_success",
|
||||||
|
"delete_success",
|
||||||
|
"view_success",
|
||||||
|
"add_task",
|
||||||
|
"change_task",
|
||||||
|
"delete_task",
|
||||||
|
"view_task",
|
||||||
|
"add_comment",
|
||||||
|
"change_comment",
|
||||||
|
"delete_comment",
|
||||||
|
"view_comment",
|
||||||
|
"add_frontendsettings",
|
||||||
|
"change_frontendsettings",
|
||||||
|
"delete_frontendsettings",
|
||||||
|
"view_frontendsettings",
|
||||||
|
"add_log",
|
||||||
|
"change_log",
|
||||||
|
"delete_log",
|
||||||
|
"view_log",
|
||||||
|
"add_savedviewfilterrule",
|
||||||
|
"change_savedviewfilterrule",
|
||||||
|
"delete_savedviewfilterrule",
|
||||||
|
"view_savedviewfilterrule",
|
||||||
|
"add_taskattributes",
|
||||||
|
"change_taskattributes",
|
||||||
|
"delete_taskattributes",
|
||||||
|
"view_taskattributes",
|
||||||
|
"add_session",
|
||||||
|
"change_session",
|
||||||
|
"delete_session",
|
||||||
|
"view_session"
|
||||||
|
],
|
||||||
|
"inherited_permissions": [
|
||||||
|
"auth.delete_permission",
|
||||||
|
"django_celery_results.add_taskresult",
|
||||||
|
"documents.view_taskattributes",
|
||||||
|
"django_q.add_ormq",
|
||||||
|
"django_q.add_success",
|
||||||
|
"django_q.delete_schedule",
|
||||||
|
"django_q.view_ormq",
|
||||||
|
"auth.view_permission",
|
||||||
|
"django_q.add_schedule",
|
||||||
|
"django_celery_results.change_taskresult",
|
||||||
|
"django_q.change_schedule",
|
||||||
|
"django_celery_results.delete_taskresult",
|
||||||
|
"authtoken.change_token",
|
||||||
|
"auth.change_group",
|
||||||
|
"documents.add_comment",
|
||||||
|
"authtoken.delete_tokenproxy",
|
||||||
|
"documents.view_documenttype",
|
||||||
|
"contenttypes.delete_contenttype",
|
||||||
|
"documents.change_correspondent",
|
||||||
|
"authtoken.delete_token",
|
||||||
|
"documents.change_log",
|
||||||
|
"auth.view_group",
|
||||||
|
"authtoken.view_token",
|
||||||
|
"django_celery_results.view_chordcounter",
|
||||||
|
"django_celery_results.view_groupresult",
|
||||||
|
"documents.delete_documenttype",
|
||||||
|
"django_q.change_ormq",
|
||||||
|
"documents.change_savedviewfilterrule",
|
||||||
|
"django_celery_results.add_groupresult",
|
||||||
|
"auth.delete_group",
|
||||||
|
"documents.add_documenttype",
|
||||||
|
"django_q.change_success",
|
||||||
|
"auth.add_permission",
|
||||||
|
"documents.delete_correspondent",
|
||||||
|
"documents.delete_savedviewfilterrule",
|
||||||
|
"documents.add_correspondent",
|
||||||
|
"authtoken.view_tokenproxy",
|
||||||
|
"documents.delete_frontendsettings",
|
||||||
|
"django_celery_results.delete_chordcounter",
|
||||||
|
"documents.add_taskattributes",
|
||||||
|
"django_q.change_task",
|
||||||
|
"sessions.add_session",
|
||||||
|
"documents.change_taskattributes",
|
||||||
|
"documents.change_comment",
|
||||||
|
"django_q.delete_task",
|
||||||
|
"django_q.delete_ormq",
|
||||||
|
"auth.change_permission",
|
||||||
|
"documents.add_savedviewfilterrule",
|
||||||
|
"django_q.view_task",
|
||||||
|
"documents.view_savedviewfilterrule",
|
||||||
|
"documents.change_frontendsettings",
|
||||||
|
"documents.change_documenttype",
|
||||||
|
"documents.view_correspondent",
|
||||||
|
"django_q.view_success",
|
||||||
|
"documents.add_frontendsettings",
|
||||||
|
"django_celery_results.delete_groupresult",
|
||||||
|
"documents.delete_savedview",
|
||||||
|
"authtoken.change_tokenproxy",
|
||||||
|
"documents.view_frontendsettings",
|
||||||
|
"authtoken.add_token",
|
||||||
|
"sessions.change_session",
|
||||||
|
"django_celery_results.add_chordcounter",
|
||||||
|
"documents.view_savedview",
|
||||||
|
"contenttypes.change_contenttype",
|
||||||
|
"django_q.delete_failure",
|
||||||
|
"authtoken.add_tokenproxy",
|
||||||
|
"documents.view_document",
|
||||||
|
"documents.add_savedview",
|
||||||
|
"django_q.view_failure",
|
||||||
|
"documents.view_comment",
|
||||||
|
"documents.view_log",
|
||||||
|
"documents.add_log",
|
||||||
|
"documents.change_savedview",
|
||||||
|
"django_q.view_schedule",
|
||||||
|
"documents.change_document",
|
||||||
|
"django_celery_results.change_chordcounter",
|
||||||
|
"documents.add_document",
|
||||||
|
"sessions.delete_session",
|
||||||
|
"django_q.change_failure",
|
||||||
|
"django_celery_results.view_taskresult",
|
||||||
|
"contenttypes.add_contenttype",
|
||||||
|
"django_q.delete_success",
|
||||||
|
"documents.delete_comment",
|
||||||
|
"django_q.add_failure",
|
||||||
|
"sessions.view_session",
|
||||||
|
"contenttypes.view_contenttype",
|
||||||
|
"documents.delete_taskattributes",
|
||||||
|
"documents.delete_document",
|
||||||
|
"documents.delete_log",
|
||||||
|
"django_q.add_task",
|
||||||
|
"django_celery_results.change_groupresult",
|
||||||
|
"auth.add_group"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"username": "testuser",
|
||||||
|
"password": "**********",
|
||||||
|
"first_name": "",
|
||||||
|
"last_name": "",
|
||||||
|
"date_joined": "2022-11-16T04:14:20.484914Z",
|
||||||
|
"is_staff": false,
|
||||||
|
"is_active": true,
|
||||||
|
"is_superuser": false,
|
||||||
|
"groups": [
|
||||||
|
1,
|
||||||
|
6
|
||||||
|
],
|
||||||
|
"user_permissions": [
|
||||||
|
"add_logentry",
|
||||||
|
"change_logentry",
|
||||||
|
"delete_logentry",
|
||||||
|
"view_logentry"
|
||||||
|
],
|
||||||
|
"inherited_permissions": [
|
||||||
|
"auth.delete_permission",
|
||||||
|
"django_celery_results.add_taskresult",
|
||||||
|
"documents.view_taskattributes",
|
||||||
|
"django_q.add_ormq",
|
||||||
|
"django_q.add_success",
|
||||||
|
"django_q.delete_schedule",
|
||||||
|
"django_q.view_ormq",
|
||||||
|
"auth.change_user",
|
||||||
|
"auth.view_permission",
|
||||||
|
"auth.view_user",
|
||||||
|
"django_q.add_schedule",
|
||||||
|
"django_celery_results.change_taskresult",
|
||||||
|
"django_q.change_schedule",
|
||||||
|
"django_celery_results.delete_taskresult",
|
||||||
|
"authtoken.change_token",
|
||||||
|
"auth.change_group",
|
||||||
|
"documents.add_comment",
|
||||||
|
"authtoken.delete_tokenproxy",
|
||||||
|
"documents.view_documenttype",
|
||||||
|
"contenttypes.delete_contenttype",
|
||||||
|
"documents.change_correspondent",
|
||||||
|
"authtoken.delete_token",
|
||||||
|
"documents.change_log",
|
||||||
|
"auth.view_group",
|
||||||
|
"authtoken.view_token",
|
||||||
|
"django_celery_results.view_chordcounter",
|
||||||
|
"django_celery_results.view_groupresult",
|
||||||
|
"documents.delete_documenttype",
|
||||||
|
"django_q.change_ormq",
|
||||||
|
"documents.change_savedviewfilterrule",
|
||||||
|
"django_celery_results.add_groupresult",
|
||||||
|
"auth.delete_group",
|
||||||
|
"documents.add_documenttype",
|
||||||
|
"django_q.change_success",
|
||||||
|
"auth.add_permission",
|
||||||
|
"documents.delete_correspondent",
|
||||||
|
"documents.delete_savedviewfilterrule",
|
||||||
|
"documents.add_correspondent",
|
||||||
|
"authtoken.view_tokenproxy",
|
||||||
|
"documents.delete_frontendsettings",
|
||||||
|
"django_celery_results.delete_chordcounter",
|
||||||
|
"documents.add_taskattributes",
|
||||||
|
"django_q.change_task",
|
||||||
|
"sessions.add_session",
|
||||||
|
"documents.change_taskattributes",
|
||||||
|
"documents.change_comment",
|
||||||
|
"django_q.delete_task",
|
||||||
|
"django_q.delete_ormq",
|
||||||
|
"auth.change_permission",
|
||||||
|
"documents.add_savedviewfilterrule",
|
||||||
|
"django_q.view_task",
|
||||||
|
"documents.view_savedviewfilterrule",
|
||||||
|
"documents.change_frontendsettings",
|
||||||
|
"documents.change_documenttype",
|
||||||
|
"documents.view_correspondent",
|
||||||
|
"auth.add_user",
|
||||||
|
"django_q.view_success",
|
||||||
|
"documents.add_frontendsettings",
|
||||||
|
"django_celery_results.delete_groupresult",
|
||||||
|
"documents.delete_savedview",
|
||||||
|
"authtoken.change_tokenproxy",
|
||||||
|
"documents.view_frontendsettings",
|
||||||
|
"authtoken.add_token",
|
||||||
|
"sessions.change_session",
|
||||||
|
"django_celery_results.add_chordcounter",
|
||||||
|
"documents.view_savedview",
|
||||||
|
"contenttypes.change_contenttype",
|
||||||
|
"django_q.delete_failure",
|
||||||
|
"authtoken.add_tokenproxy",
|
||||||
|
"documents.view_document",
|
||||||
|
"documents.add_savedview",
|
||||||
|
"django_q.view_failure",
|
||||||
|
"documents.view_comment",
|
||||||
|
"documents.view_log",
|
||||||
|
"auth.delete_user",
|
||||||
|
"documents.add_log",
|
||||||
|
"documents.change_savedview",
|
||||||
|
"django_q.view_schedule",
|
||||||
|
"documents.change_document",
|
||||||
|
"django_celery_results.change_chordcounter",
|
||||||
|
"documents.add_document",
|
||||||
|
"sessions.delete_session",
|
||||||
|
"django_q.change_failure",
|
||||||
|
"django_celery_results.view_taskresult",
|
||||||
|
"contenttypes.add_contenttype",
|
||||||
|
"django_q.delete_success",
|
||||||
|
"documents.delete_comment",
|
||||||
|
"django_q.add_failure",
|
||||||
|
"sessions.view_session",
|
||||||
|
"contenttypes.view_contenttype",
|
||||||
|
"documents.delete_taskattributes",
|
||||||
|
"documents.delete_document",
|
||||||
|
"documents.delete_log",
|
||||||
|
"django_q.add_task",
|
||||||
|
"django_celery_results.change_groupresult",
|
||||||
|
"auth.add_group"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -5,6 +5,14 @@ beforeEach(() => {
|
|||||||
fixture: 'ui_settings/settings.json',
|
fixture: 'ui_settings/settings.json',
|
||||||
}).as('ui-settings')
|
}).as('ui-settings')
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/users/*', {
|
||||||
|
fixture: 'users/users.json',
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/groups/*', {
|
||||||
|
fixture: 'groups/groups.json',
|
||||||
|
})
|
||||||
|
|
||||||
cy.intercept('http://localhost:8000/api/remote_version/', {
|
cy.intercept('http://localhost:8000/api/remote_version/', {
|
||||||
fixture: 'remote_version/remote_version.json',
|
fixture: 'remote_version/remote_version.json',
|
||||||
})
|
})
|
||||||
|
1295
src-ui/messages.xlf
1295
src-ui/messages.xlf
File diff suppressed because it is too large
Load Diff
@ -14,8 +14,13 @@ import { DocumentAsnComponent } from './components/document-asn/document-asn.com
|
|||||||
import { DirtyFormGuard } from './guards/dirty-form.guard'
|
import { DirtyFormGuard } from './guards/dirty-form.guard'
|
||||||
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
|
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
|
||||||
import { TasksComponent } from './components/manage/tasks/tasks.component'
|
import { TasksComponent } from './components/manage/tasks/tasks.component'
|
||||||
|
import { PermissionsGuard } from './guards/permissions.guard'
|
||||||
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
||||||
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
|
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
|
||||||
|
import {
|
||||||
|
PermissionAction,
|
||||||
|
PermissionType,
|
||||||
|
} from './services/permissions.service'
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
|
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
|
||||||
@ -29,23 +34,137 @@ const routes: Routes = [
|
|||||||
path: 'documents',
|
path: 'documents',
|
||||||
component: DocumentListComponent,
|
component: DocumentListComponent,
|
||||||
canDeactivate: [DirtySavedViewGuard],
|
canDeactivate: [DirtySavedViewGuard],
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.Document,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'view/:id',
|
path: 'view/:id',
|
||||||
component: DocumentListComponent,
|
component: DocumentListComponent,
|
||||||
canDeactivate: [DirtySavedViewGuard],
|
canDeactivate: [DirtySavedViewGuard],
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.SavedView,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'documents/:id',
|
||||||
|
component: DocumentDetailComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.Document,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'asn/:id',
|
||||||
|
component: DocumentAsnComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.Document,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tags',
|
||||||
|
component: TagListComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.Tag,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'documenttypes',
|
||||||
|
component: DocumentTypeListComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.DocumentType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'correspondents',
|
||||||
|
component: CorrespondentListComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.Correspondent,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'storagepaths',
|
||||||
|
component: StoragePathListComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.StoragePath,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'logs',
|
||||||
|
component: LogsComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.Admin,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{ path: 'documents/:id', component: DocumentDetailComponent },
|
|
||||||
{ path: 'asn/:id', component: DocumentAsnComponent },
|
|
||||||
{ path: 'tags', component: TagListComponent },
|
|
||||||
{ path: 'documenttypes', component: DocumentTypeListComponent },
|
|
||||||
{ path: 'correspondents', component: CorrespondentListComponent },
|
|
||||||
{ path: 'storagepaths', component: StoragePathListComponent },
|
|
||||||
{ path: 'logs', component: LogsComponent },
|
|
||||||
{
|
{
|
||||||
path: 'settings',
|
path: 'settings',
|
||||||
component: SettingsComponent,
|
component: SettingsComponent,
|
||||||
canDeactivate: [DirtyFormGuard],
|
canDeactivate: [DirtyFormGuard],
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.UISettings,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tasks',
|
||||||
|
component: TasksComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.PaperlessTask,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'settings/:section',
|
||||||
|
component: SettingsComponent,
|
||||||
|
canDeactivate: [DirtyFormGuard],
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.UISettings,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'settings/:section',
|
path: 'settings/:section',
|
||||||
|
@ -9,6 +9,11 @@ import { NgxFileDropEntry } from 'ngx-file-drop'
|
|||||||
import { UploadDocumentsService } from './services/upload-documents.service'
|
import { UploadDocumentsService } from './services/upload-documents.service'
|
||||||
import { TasksService } from './services/tasks.service'
|
import { TasksService } from './services/tasks.service'
|
||||||
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
|
import {
|
||||||
|
PermissionAction,
|
||||||
|
PermissionsService,
|
||||||
|
PermissionType,
|
||||||
|
} from './services/permissions.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@ -32,7 +37,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
private uploadDocumentsService: UploadDocumentsService,
|
private uploadDocumentsService: UploadDocumentsService,
|
||||||
private tasksService: TasksService,
|
private tasksService: TasksService,
|
||||||
public tourService: TourService,
|
public tourService: TourService,
|
||||||
private renderer: Renderer2
|
private renderer: Renderer2,
|
||||||
|
private permissionsService: PermissionsService
|
||||||
) {
|
) {
|
||||||
let anyWindow = window as any
|
let anyWindow = window as any
|
||||||
anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.js'
|
anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.js'
|
||||||
@ -74,15 +80,28 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
if (
|
if (
|
||||||
this.showNotification(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS)
|
this.showNotification(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS)
|
||||||
) {
|
) {
|
||||||
this.toastService.show({
|
if (
|
||||||
title: $localize`Document added`,
|
this.permissionsService.currentUserCan(
|
||||||
delay: 10000,
|
PermissionAction.View,
|
||||||
content: $localize`Document ${status.filename} was added to paperless.`,
|
PermissionType.Document
|
||||||
actionName: $localize`Open document`,
|
)
|
||||||
action: () => {
|
) {
|
||||||
this.router.navigate(['documents', status.documentId])
|
this.toastService.show({
|
||||||
},
|
title: $localize`Document added`,
|
||||||
})
|
delay: 10000,
|
||||||
|
content: $localize`Document ${status.filename} was added to paperless.`,
|
||||||
|
actionName: $localize`Open document`,
|
||||||
|
action: () => {
|
||||||
|
this.router.navigate(['documents', status.documentId])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.toastService.show({
|
||||||
|
title: $localize`Document added`,
|
||||||
|
delay: 10000,
|
||||||
|
content: $localize`Document ${status.filename} was added to paperless.`,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -225,7 +244,13 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get dragDropEnabled(): boolean {
|
public get dragDropEnabled(): boolean {
|
||||||
return !this.router.url.includes('dashboard')
|
return (
|
||||||
|
!this.router.url.includes('dashboard') &&
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.Add,
|
||||||
|
PermissionType.Document
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public fileOver() {
|
public fileOver() {
|
||||||
|
@ -42,6 +42,7 @@ import { CheckComponent } from './components/common/input/check/check.component'
|
|||||||
import { PasswordComponent } from './components/common/input/password/password.component'
|
import { PasswordComponent } from './components/common/input/password/password.component'
|
||||||
import { SaveViewConfigDialogComponent } from './components/document-list/save-view-config-dialog/save-view-config-dialog.component'
|
import { SaveViewConfigDialogComponent } from './components/document-list/save-view-config-dialog/save-view-config-dialog.component'
|
||||||
import { TagsComponent } from './components/common/input/tags/tags.component'
|
import { TagsComponent } from './components/common/input/tags/tags.component'
|
||||||
|
import { IfPermissionsDirective } from './directives/if-permissions.directive'
|
||||||
import { SortableDirective } from './directives/sortable.directive'
|
import { SortableDirective } from './directives/sortable.directive'
|
||||||
import { CookieService } from 'ngx-cookie-service'
|
import { CookieService } from 'ngx-cookie-service'
|
||||||
import { CsrfInterceptor } from './interceptors/csrf.interceptor'
|
import { CsrfInterceptor } from './interceptors/csrf.interceptor'
|
||||||
@ -70,6 +71,7 @@ import { ColorSliderModule } from 'ngx-color/slider'
|
|||||||
import { ColorComponent } from './components/common/input/color/color.component'
|
import { ColorComponent } from './components/common/input/color/color.component'
|
||||||
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
||||||
import { DocumentCommentsComponent } from './components/document-comments/document-comments.component'
|
import { DocumentCommentsComponent } from './components/document-comments/document-comments.component'
|
||||||
|
import { PermissionsGuard } from './guards/permissions.guard'
|
||||||
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
||||||
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
|
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
|
||||||
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
|
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
|
||||||
@ -77,8 +79,15 @@ import { StoragePathEditDialogComponent } from './components/common/edit-dialog/
|
|||||||
import { SettingsService } from './services/settings.service'
|
import { SettingsService } from './services/settings.service'
|
||||||
import { TasksComponent } from './components/manage/tasks/tasks.component'
|
import { TasksComponent } from './components/manage/tasks/tasks.component'
|
||||||
import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
|
import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
|
import { UserEditDialogComponent } from './components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
|
||||||
|
import { GroupEditDialogComponent } from './components/common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
|
||||||
|
import { PermissionsSelectComponent } from './components/common/permissions-select/permissions-select.component'
|
||||||
import { MailAccountEditDialogComponent } from './components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component'
|
import { MailAccountEditDialogComponent } from './components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component'
|
||||||
import { MailRuleEditDialogComponent } from './components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component'
|
import { MailRuleEditDialogComponent } from './components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component'
|
||||||
|
import { PermissionsUserComponent } from './components/common/input/permissions/permissions-user/permissions-user.component'
|
||||||
|
import { PermissionsGroupComponent } from './components/common/input/permissions/permissions-group/permissions-group.component'
|
||||||
|
import { IfOwnerDirective } from './directives/if-owner.directive'
|
||||||
|
import { IfObjectPermissionsDirective } from './directives/if-object-permissions.directive'
|
||||||
|
|
||||||
import localeAr from '@angular/common/locales/ar'
|
import localeAr from '@angular/common/locales/ar'
|
||||||
import localeBe from '@angular/common/locales/be'
|
import localeBe from '@angular/common/locales/be'
|
||||||
@ -100,6 +109,8 @@ import localeSr from '@angular/common/locales/sr'
|
|||||||
import localeSv from '@angular/common/locales/sv'
|
import localeSv from '@angular/common/locales/sv'
|
||||||
import localeTr from '@angular/common/locales/tr'
|
import localeTr from '@angular/common/locales/tr'
|
||||||
import localeZh from '@angular/common/locales/zh'
|
import localeZh from '@angular/common/locales/zh'
|
||||||
|
import { PermissionsDialogComponent } from './components/common/permissions-dialog/permissions-dialog.component'
|
||||||
|
import { PermissionsFormComponent } from './components/common/input/permissions/permissions-form/permissions-form.component'
|
||||||
|
|
||||||
registerLocaleData(localeAr)
|
registerLocaleData(localeAr)
|
||||||
registerLocaleData(localeBe)
|
registerLocaleData(localeBe)
|
||||||
@ -165,6 +176,7 @@ function initializeApp(settings: SettingsService) {
|
|||||||
PasswordComponent,
|
PasswordComponent,
|
||||||
SaveViewConfigDialogComponent,
|
SaveViewConfigDialogComponent,
|
||||||
TagsComponent,
|
TagsComponent,
|
||||||
|
IfPermissionsDirective,
|
||||||
SortableDirective,
|
SortableDirective,
|
||||||
SavedViewWidgetComponent,
|
SavedViewWidgetComponent,
|
||||||
StatisticsWidgetComponent,
|
StatisticsWidgetComponent,
|
||||||
@ -186,8 +198,17 @@ function initializeApp(settings: SettingsService) {
|
|||||||
DocumentAsnComponent,
|
DocumentAsnComponent,
|
||||||
DocumentCommentsComponent,
|
DocumentCommentsComponent,
|
||||||
TasksComponent,
|
TasksComponent,
|
||||||
|
UserEditDialogComponent,
|
||||||
|
GroupEditDialogComponent,
|
||||||
|
PermissionsSelectComponent,
|
||||||
MailAccountEditDialogComponent,
|
MailAccountEditDialogComponent,
|
||||||
MailRuleEditDialogComponent,
|
MailRuleEditDialogComponent,
|
||||||
|
PermissionsUserComponent,
|
||||||
|
PermissionsGroupComponent,
|
||||||
|
IfOwnerDirective,
|
||||||
|
IfObjectPermissionsDirective,
|
||||||
|
PermissionsDialogComponent,
|
||||||
|
PermissionsFormComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
@ -225,6 +246,7 @@ function initializeApp(settings: SettingsService) {
|
|||||||
DocumentTitlePipe,
|
DocumentTitlePipe,
|
||||||
{ provide: NgbDateAdapter, useClass: ISODateAdapter },
|
{ provide: NgbDateAdapter, useClass: ISODateAdapter },
|
||||||
{ provide: NgbDateParserFormatter, useClass: LocalizedDateParserFormatter },
|
{ provide: NgbDateParserFormatter, useClass: LocalizedDateParserFormatter },
|
||||||
|
PermissionsGuard,
|
||||||
DirtyDocGuard,
|
DirtyDocGuard,
|
||||||
DirtySavedViewGuard,
|
DirtySavedViewGuard,
|
||||||
],
|
],
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span class="ms-2" [class.visually-hidden]="slimSidebarEnabled" i18n="app title">Paperless-ngx</span>
|
<span class="ms-2" [class.visually-hidden]="slimSidebarEnabled" i18n="app title">Paperless-ngx</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 ps-md-4 me-sm-auto order-3 order-sm-1">
|
<div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 ps-md-4 me-sm-auto order-3 order-sm-1" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||||
<form (ngSubmit)="search()" class="form-inline flex-grow-1">
|
<form (ngSubmit)="search()" class="form-inline flex-grow-1">
|
||||||
<svg width="1em" height="1em" fill="currentColor">
|
<svg width="1em" height="1em" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#search"/>
|
<use xlink:href="assets/bootstrap-icons.svg#search"/>
|
||||||
@ -39,7 +39,7 @@
|
|||||||
<p class="small mb-0 px-3 text-muted" i18n>Logged in as {{this.settingsService.displayName}}</p>
|
<p class="small mb-0 px-3 text-muted" i18n>Logged in as {{this.settingsService.displayName}}</p>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
</div>
|
</div>
|
||||||
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()">
|
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }">
|
||||||
<svg class="sidebaricon me-2" fill="currentColor">
|
<svg class="sidebaricon me-2" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
|
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
|
||||||
</svg><ng-container i18n>Settings</ng-container>
|
</svg><ng-container i18n>Settings</ng-container>
|
||||||
@ -72,7 +72,7 @@
|
|||||||
</svg><span> <ng-container i18n>Dashboard</ng-container></span>
|
</svg><span> <ng-container i18n>Dashboard</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||||
<a class="nav-link" routerLink="documents" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Documents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
<a class="nav-link" routerLink="documents" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Documents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#files"/>
|
<use xlink:href="assets/bootstrap-icons.svg#files"/>
|
||||||
@ -80,79 +80,82 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<div *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
|
||||||
|
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted" *ngIf='savedViewService.loading || savedViewService.sidebarViews.length > 0'>
|
||||||
|
<span i18n>Saved views</span>
|
||||||
|
<div *ngIf="savedViewService.loading" class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div>
|
||||||
|
</h6>
|
||||||
|
<ul class="nav flex-column mb-2">
|
||||||
|
<li class="nav-item w-100" *ngFor="let view of savedViewService.sidebarViews">
|
||||||
|
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="view/{{view.id}}" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name" [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#funnel"/>
|
||||||
|
</svg><span> {{view.name}}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted" *ngIf='savedViewService.loading || savedViewService.sidebarViews.length > 0'>
|
<div *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||||
<span i18n>Saved views</span>
|
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted" *ngIf='openDocuments.length > 0'>
|
||||||
<div *ngIf="savedViewService.loading" class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div>
|
<span i18n>Open documents</span>
|
||||||
</h6>
|
</h6>
|
||||||
<ul class="nav flex-column mb-2">
|
<ul class="nav flex-column mb-2">
|
||||||
<li class="nav-item w-100" *ngFor="let view of savedViewService.sidebarViews">
|
<li class="nav-item w-100" *ngFor='let d of openDocuments'>
|
||||||
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="view/{{view.id}}" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name" [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="documents/{{d.id}}" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="d.title | documentTitle" [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#funnel"/>
|
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
|
||||||
</svg><span> {{view.name}}</span>
|
</svg><span> {{d.title | documentTitle}}</span>
|
||||||
</a>
|
<span class="close" (click)="closeDocument(d); $event.preventDefault()">
|
||||||
</li>
|
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-x" viewBox="0 0 16 16">
|
||||||
</ul>
|
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||||
|
</svg>
|
||||||
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted" *ngIf='openDocuments.length > 0'>
|
</span>
|
||||||
<span i18n>Open documents</span>
|
</a>
|
||||||
</h6>
|
</li>
|
||||||
<ul class="nav flex-column mb-2">
|
<li class="nav-item w-100" *ngIf="openDocuments.length >= 1">
|
||||||
<li class="nav-item w-100" *ngFor='let d of openDocuments'>
|
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" [routerLink]="[]" (click)="closeAll()" ngbPopover="Close all" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="documents/{{d.id}}" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="d.title | documentTitle" [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
|
|
||||||
</svg><span> {{d.title | documentTitle}}</span>
|
|
||||||
<span class="close" (click)="closeDocument(d); $event.preventDefault()">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-x" viewBox="0 0 16 16">
|
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||||
</svg>
|
</svg><span> <ng-container i18n>Close all</ng-container></span>
|
||||||
</span>
|
</a>
|
||||||
</a>
|
</li>
|
||||||
</li>
|
</ul>
|
||||||
<li class="nav-item w-100" *ngIf="openDocuments.length >= 1">
|
</div>
|
||||||
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" [routerLink]="[]" (click)="closeAll()" ngbPopover="Close all" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
|
||||||
</svg><span> <ng-container i18n>Close all</ng-container></span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted">
|
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted">
|
||||||
<span i18n>Manage</span>
|
<span i18n>Manage</span>
|
||||||
</h6>
|
</h6>
|
||||||
<ul class="nav flex-column mb-2">
|
<ul class="nav flex-column mb-2">
|
||||||
<li class="nav-item">
|
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
|
||||||
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Correspondents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Correspondents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#person"/>
|
<use xlink:href="assets/bootstrap-icons.svg#person"/>
|
||||||
</svg><span> <ng-container i18n>Correspondents</ng-container></span>
|
</svg><span> <ng-container i18n>Correspondents</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" tourAnchor="tour.tags">
|
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }" tourAnchor="tour.tags">
|
||||||
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Tags" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Tags" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#tags"/>
|
<use xlink:href="assets/bootstrap-icons.svg#tags"/>
|
||||||
</svg><span> <ng-container i18n>Tags</ng-container></span>
|
</svg><span> <ng-container i18n>Tags</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
|
||||||
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Document types" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Document types" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#hash"/>
|
<use xlink:href="assets/bootstrap-icons.svg#hash"/>
|
||||||
</svg><span> <ng-container i18n>Document types</ng-container></span>
|
</svg><span> <ng-container i18n>Document types</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
|
||||||
<a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Storage paths" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
<a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Storage paths" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#folder"/>
|
<use xlink:href="assets/bootstrap-icons.svg#folder"/>
|
||||||
</svg><span> <ng-container i18n>Storage paths</ng-container></span>
|
</svg><span> <ng-container i18n>Storage paths</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" tourAnchor="tour.file-tasks">
|
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.PaperlessTask }" tourAnchor="tour.file-tasks">
|
||||||
<a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()" ngbPopover="File Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
<a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()" ngbPopover="File Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<span *ngIf="tasksService.failedFileTasks.length > 0 && slimSidebarEnabled" class="badge bg-danger position-absolute top-0 end-0">{{tasksService.failedFileTasks.length}}</span>
|
<span *ngIf="tasksService.failedFileTasks.length > 0 && slimSidebarEnabled" class="badge bg-danger position-absolute top-0 end-0">{{tasksService.failedFileTasks.length}}</span>
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
@ -160,14 +163,14 @@
|
|||||||
</svg><span> <ng-container i18n>File Tasks<span *ngIf="tasksService.failedFileTasks.length > 0"><span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span></span></ng-container></span>
|
</svg><span> <ng-container i18n>File Tasks<span *ngIf="tasksService.failedFileTasks.length > 0"><span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span></span></ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item" *appIfPermissions="{ 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">
|
<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">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#text-left"/>
|
<use xlink:href="assets/bootstrap-icons.svg#text-left"/>
|
||||||
</svg><span> <ng-container i18n>Logs</ng-container></span>
|
</svg><span> <ng-container i18n>Logs</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" tourAnchor="tour.settings">
|
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }" tourAnchor="tour.settings">
|
||||||
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Settings" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Settings" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
|
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
|
||||||
|
@ -26,13 +26,17 @@ import { TasksService } from 'src/app/services/tasks.service'
|
|||||||
import { ComponentCanDeactivate } from 'src/app/guards/dirty-doc.guard'
|
import { ComponentCanDeactivate } from 'src/app/guards/dirty-doc.guard'
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-app-frame',
|
selector: 'app-app-frame',
|
||||||
templateUrl: './app-frame.component.html',
|
templateUrl: './app-frame.component.html',
|
||||||
styleUrls: ['./app-frame.component.scss'],
|
styleUrls: ['./app-frame.component.scss'],
|
||||||
})
|
})
|
||||||
export class AppFrameComponent implements OnInit, ComponentCanDeactivate {
|
export class AppFrameComponent
|
||||||
|
extends ComponentWithPermissions
|
||||||
|
implements OnInit, ComponentCanDeactivate
|
||||||
|
{
|
||||||
constructor(
|
constructor(
|
||||||
public router: Router,
|
public router: Router,
|
||||||
private activatedRoute: ActivatedRoute,
|
private activatedRoute: ActivatedRoute,
|
||||||
@ -44,7 +48,9 @@ export class AppFrameComponent implements OnInit, ComponentCanDeactivate {
|
|||||||
public settingsService: SettingsService,
|
public settingsService: SettingsService,
|
||||||
public tasksService: TasksService,
|
public tasksService: TasksService,
|
||||||
private readonly toastService: ToastService
|
private readonly toastService: ToastService
|
||||||
) {}
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (this.settingsService.get(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED)) {
|
if (this.settingsService.get(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED)) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { interval, Subject, switchMap, take } from 'rxjs'
|
import { interval, Subject, take } from 'rxjs'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-confirm-dialog',
|
selector: 'app-confirm-dialog',
|
||||||
|
@ -5,10 +5,16 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|
||||||
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
||||||
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
||||||
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text>
|
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text>
|
||||||
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive" novalidate></app-input-check>
|
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive" novalidate></app-input-check>
|
||||||
|
|
||||||
|
<div *appIfOwner="object">
|
||||||
|
<app-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></app-permissions-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||||
|
@ -5,6 +5,7 @@ import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-
|
|||||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
||||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||||
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-correspondent-edit-dialog',
|
selector: 'app-correspondent-edit-dialog',
|
||||||
@ -12,8 +13,12 @@ import { CorrespondentService } from 'src/app/services/rest/correspondent.servic
|
|||||||
styleUrls: ['./correspondent-edit-dialog.component.scss'],
|
styleUrls: ['./correspondent-edit-dialog.component.scss'],
|
||||||
})
|
})
|
||||||
export class CorrespondentEditDialogComponent extends EditDialogComponent<PaperlessCorrespondent> {
|
export class CorrespondentEditDialogComponent extends EditDialogComponent<PaperlessCorrespondent> {
|
||||||
constructor(service: CorrespondentService, activeModal: NgbActiveModal) {
|
constructor(
|
||||||
super(service, activeModal)
|
service: CorrespondentService,
|
||||||
|
activeModal: NgbActiveModal,
|
||||||
|
userService: UserService
|
||||||
|
) {
|
||||||
|
super(service, activeModal, userService)
|
||||||
}
|
}
|
||||||
|
|
||||||
getCreateTitle() {
|
getCreateTitle() {
|
||||||
@ -30,6 +35,7 @@ export class CorrespondentEditDialogComponent extends EditDialogComponent<Paperl
|
|||||||
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
||||||
match: new FormControl(''),
|
match: new FormControl(''),
|
||||||
is_insensitive: new FormControl(true),
|
is_insensitive: new FormControl(true),
|
||||||
|
permissions_form: new FormControl(null),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,10 +6,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|
||||||
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
<div class="col">
|
||||||
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
||||||
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text>
|
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
||||||
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check>
|
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text>
|
||||||
|
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *appIfOwner="object">
|
||||||
|
<app-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></app-permissions-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
@ -5,6 +5,7 @@ import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-
|
|||||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
|
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
|
||||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||||
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-document-type-edit-dialog',
|
selector: 'app-document-type-edit-dialog',
|
||||||
@ -12,8 +13,12 @@ import { DocumentTypeService } from 'src/app/services/rest/document-type.service
|
|||||||
styleUrls: ['./document-type-edit-dialog.component.scss'],
|
styleUrls: ['./document-type-edit-dialog.component.scss'],
|
||||||
})
|
})
|
||||||
export class DocumentTypeEditDialogComponent extends EditDialogComponent<PaperlessDocumentType> {
|
export class DocumentTypeEditDialogComponent extends EditDialogComponent<PaperlessDocumentType> {
|
||||||
constructor(service: DocumentTypeService, activeModal: NgbActiveModal) {
|
constructor(
|
||||||
super(service, activeModal)
|
service: DocumentTypeService,
|
||||||
|
activeModal: NgbActiveModal,
|
||||||
|
userService: UserService
|
||||||
|
) {
|
||||||
|
super(service, activeModal, userService)
|
||||||
}
|
}
|
||||||
|
|
||||||
getCreateTitle() {
|
getCreateTitle() {
|
||||||
@ -30,6 +35,7 @@ export class DocumentTypeEditDialogComponent extends EditDialogComponent<Paperle
|
|||||||
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
||||||
match: new FormControl(''),
|
match: new FormControl(''),
|
||||||
is_insensitive: new FormControl(true),
|
is_insensitive: new FormControl(true),
|
||||||
|
permissions_form: new FormControl(null),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,17 +4,25 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
|||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
import { MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model'
|
import { MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model'
|
||||||
import { ObjectWithId } from 'src/app/data/object-with-id'
|
import { ObjectWithId } from 'src/app/data/object-with-id'
|
||||||
|
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
|
||||||
|
import { PaperlessUser } from 'src/app/data/paperless-user'
|
||||||
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'
|
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'
|
||||||
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
|
import { PermissionsFormObject } from '../input/permissions/permissions-form/permissions-form.component'
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export abstract class EditDialogComponent<T extends ObjectWithId>
|
export abstract class EditDialogComponent<
|
||||||
implements OnInit
|
T extends ObjectWithPermissions | ObjectWithId
|
||||||
|
> implements OnInit
|
||||||
{
|
{
|
||||||
constructor(
|
constructor(
|
||||||
private service: AbstractPaperlessService<T>,
|
private service: AbstractPaperlessService<T>,
|
||||||
private activeModal: NgbActiveModal
|
private activeModal: NgbActiveModal,
|
||||||
|
private userService: UserService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
users: PaperlessUser[]
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
dialogMode: string = 'create'
|
dialogMode: string = 'create'
|
||||||
|
|
||||||
@ -36,6 +44,14 @@ export abstract class EditDialogComponent<T extends ObjectWithId>
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (this.object != null) {
|
if (this.object != null) {
|
||||||
|
if (this.object['permissions']) {
|
||||||
|
this.object['set_permissions'] = this.object['permissions']
|
||||||
|
}
|
||||||
|
|
||||||
|
this.object['permissions_form'] = {
|
||||||
|
owner: (this.object as ObjectWithPermissions).owner,
|
||||||
|
set_permissions: (this.object as ObjectWithPermissions).permissions,
|
||||||
|
}
|
||||||
this.objectForm.patchValue(this.object)
|
this.objectForm.patchValue(this.object)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,6 +59,8 @@ export abstract class EditDialogComponent<T extends ObjectWithId>
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.closeEnabled = true
|
this.closeEnabled = true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.userService.listAll().subscribe((r) => (this.users = r.results))
|
||||||
}
|
}
|
||||||
|
|
||||||
getCreateTitle() {
|
getCreateTitle() {
|
||||||
@ -77,10 +95,16 @@ export abstract class EditDialogComponent<T extends ObjectWithId>
|
|||||||
}
|
}
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
var newObject = Object.assign(
|
const formValues = Object.assign({}, this.objectForm.value)
|
||||||
Object.assign({}, this.object),
|
const permissionsObject: PermissionsFormObject =
|
||||||
this.objectForm.value
|
this.objectForm.get('permissions_form')?.value
|
||||||
)
|
if (permissionsObject) {
|
||||||
|
formValues.owner = permissionsObject.owner
|
||||||
|
formValues.set_permissions = permissionsObject.set_permissions
|
||||||
|
delete formValues.permissions_form
|
||||||
|
}
|
||||||
|
|
||||||
|
var newObject = Object.assign(Object.assign({}, this.object), formValues)
|
||||||
var serverResponse: Observable<T>
|
var serverResponse: Observable<T>
|
||||||
switch (this.dialogMode) {
|
switch (this.dialogMode) {
|
||||||
case 'create':
|
case 'create':
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
<form [formGroup]="objectForm" (ngSubmit)="save()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||||
|
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
||||||
|
<app-permissions-select i18n-title title="Permissions" formControlName="permissions" [error]="error?.permissions"></app-permissions-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
@ -0,0 +1,37 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { FormControl, FormGroup } from '@angular/forms'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||||
|
import { PaperlessGroup } from 'src/app/data/paperless-group'
|
||||||
|
import { GroupService } from 'src/app/services/rest/group.service'
|
||||||
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-group-edit-dialog',
|
||||||
|
templateUrl: './group-edit-dialog.component.html',
|
||||||
|
styleUrls: ['./group-edit-dialog.component.scss'],
|
||||||
|
})
|
||||||
|
export class GroupEditDialogComponent extends EditDialogComponent<PaperlessGroup> {
|
||||||
|
constructor(
|
||||||
|
service: GroupService,
|
||||||
|
activeModal: NgbActiveModal,
|
||||||
|
userService: UserService
|
||||||
|
) {
|
||||||
|
super(service, activeModal, userService)
|
||||||
|
}
|
||||||
|
|
||||||
|
getCreateTitle() {
|
||||||
|
return $localize`Create new user group`
|
||||||
|
}
|
||||||
|
|
||||||
|
getEditTitle() {
|
||||||
|
return $localize`Edit user group`
|
||||||
|
}
|
||||||
|
|
||||||
|
getForm(): FormGroup {
|
||||||
|
return new FormGroup({
|
||||||
|
name: new FormControl(''),
|
||||||
|
permissions: new FormControl(null),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ import {
|
|||||||
PaperlessMailAccount,
|
PaperlessMailAccount,
|
||||||
} from 'src/app/data/paperless-mail-account'
|
} from 'src/app/data/paperless-mail-account'
|
||||||
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
|
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
|
||||||
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
|
|
||||||
const IMAP_SECURITY_OPTIONS = [
|
const IMAP_SECURITY_OPTIONS = [
|
||||||
{ id: IMAPSecurity.None, name: $localize`No encryption` },
|
{ id: IMAPSecurity.None, name: $localize`No encryption` },
|
||||||
@ -20,8 +21,12 @@ const IMAP_SECURITY_OPTIONS = [
|
|||||||
styleUrls: ['./mail-account-edit-dialog.component.scss'],
|
styleUrls: ['./mail-account-edit-dialog.component.scss'],
|
||||||
})
|
})
|
||||||
export class MailAccountEditDialogComponent extends EditDialogComponent<PaperlessMailAccount> {
|
export class MailAccountEditDialogComponent extends EditDialogComponent<PaperlessMailAccount> {
|
||||||
constructor(service: MailAccountService, activeModal: NgbActiveModal) {
|
constructor(
|
||||||
super(service, activeModal)
|
service: MailAccountService,
|
||||||
|
activeModal: NgbActiveModal,
|
||||||
|
userService: UserService
|
||||||
|
) {
|
||||||
|
super(service, activeModal, userService)
|
||||||
}
|
}
|
||||||
|
|
||||||
getCreateTitle() {
|
getCreateTitle() {
|
||||||
|
@ -18,6 +18,7 @@ import { CorrespondentService } from 'src/app/services/rest/correspondent.servic
|
|||||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||||
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
|
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
|
||||||
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
|
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
|
||||||
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
|
|
||||||
const ATTACHMENT_TYPE_OPTIONS = [
|
const ATTACHMENT_TYPE_OPTIONS = [
|
||||||
{
|
{
|
||||||
@ -113,9 +114,10 @@ export class MailRuleEditDialogComponent extends EditDialogComponent<PaperlessMa
|
|||||||
activeModal: NgbActiveModal,
|
activeModal: NgbActiveModal,
|
||||||
accountService: MailAccountService,
|
accountService: MailAccountService,
|
||||||
correspondentService: CorrespondentService,
|
correspondentService: CorrespondentService,
|
||||||
documentTypeService: DocumentTypeService
|
documentTypeService: DocumentTypeService,
|
||||||
|
userService: UserService
|
||||||
) {
|
) {
|
||||||
super(service, activeModal)
|
super(service, activeModal, userService)
|
||||||
|
|
||||||
accountService
|
accountService
|
||||||
.listAll()
|
.listAll()
|
||||||
|
@ -6,16 +6,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|
||||||
<p *ngIf="this.dialogMode === 'edit'" i18n>
|
|
||||||
<em>Note that editing a path does not apply changes to stored files until you have run the 'document_renamer' utility. See the <a target="_blank" href="https://docs.paperless-ngx.com/administration/#renamer">documentation</a>.</em>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
||||||
<app-input-text i18n-title title="Path" formControlName="path" [error]="error?.path" [hint]="pathHint"></app-input-text>
|
<app-input-text i18n-title title="Path" formControlName="path" [error]="error?.path" [hint]="pathHint"></app-input-text>
|
||||||
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
||||||
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text>
|
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text>
|
||||||
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check>
|
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check>
|
||||||
|
|
||||||
|
<div *appIfOwner="object">
|
||||||
|
<app-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></app-permissions-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||||
|
@ -5,6 +5,7 @@ import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-
|
|||||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||||
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
||||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||||
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-storage-path-edit-dialog',
|
selector: 'app-storage-path-edit-dialog',
|
||||||
@ -12,8 +13,12 @@ import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
|||||||
styleUrls: ['./storage-path-edit-dialog.component.scss'],
|
styleUrls: ['./storage-path-edit-dialog.component.scss'],
|
||||||
})
|
})
|
||||||
export class StoragePathEditDialogComponent extends EditDialogComponent<PaperlessStoragePath> {
|
export class StoragePathEditDialogComponent extends EditDialogComponent<PaperlessStoragePath> {
|
||||||
constructor(service: StoragePathService, activeModal: NgbActiveModal) {
|
constructor(
|
||||||
super(service, activeModal)
|
service: StoragePathService,
|
||||||
|
activeModal: NgbActiveModal,
|
||||||
|
userService: UserService
|
||||||
|
) {
|
||||||
|
super(service, activeModal, userService)
|
||||||
}
|
}
|
||||||
|
|
||||||
get pathHint() {
|
get pathHint() {
|
||||||
@ -41,6 +46,7 @@ export class StoragePathEditDialogComponent extends EditDialogComponent<Paperles
|
|||||||
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
||||||
match: new FormControl(''),
|
match: new FormControl(''),
|
||||||
is_insensitive: new FormControl(true),
|
is_insensitive: new FormControl(true),
|
||||||
|
permissions_form: new FormControl(null),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,11 @@
|
|||||||
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
||||||
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text>
|
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text>
|
||||||
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check>
|
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check>
|
||||||
|
|
||||||
|
<div *appIfOwner="object">
|
||||||
|
<app-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></app-permissions-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||||
|
@ -6,6 +6,7 @@ import { PaperlessTag } from 'src/app/data/paperless-tag'
|
|||||||
import { TagService } from 'src/app/services/rest/tag.service'
|
import { TagService } from 'src/app/services/rest/tag.service'
|
||||||
import { randomColor } from 'src/app/utils/color'
|
import { randomColor } from 'src/app/utils/color'
|
||||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||||
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-tag-edit-dialog',
|
selector: 'app-tag-edit-dialog',
|
||||||
@ -13,8 +14,12 @@ import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
|||||||
styleUrls: ['./tag-edit-dialog.component.scss'],
|
styleUrls: ['./tag-edit-dialog.component.scss'],
|
||||||
})
|
})
|
||||||
export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> {
|
export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> {
|
||||||
constructor(service: TagService, activeModal: NgbActiveModal) {
|
constructor(
|
||||||
super(service, activeModal)
|
service: TagService,
|
||||||
|
activeModal: NgbActiveModal,
|
||||||
|
userService: UserService
|
||||||
|
) {
|
||||||
|
super(service, activeModal, userService)
|
||||||
}
|
}
|
||||||
|
|
||||||
getCreateTitle() {
|
getCreateTitle() {
|
||||||
@ -33,6 +38,7 @@ export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> {
|
|||||||
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
||||||
match: new FormControl(''),
|
match: new FormControl(''),
|
||||||
is_insensitive: new FormControl(true),
|
is_insensitive: new FormControl(true),
|
||||||
|
permissions_form: new FormControl(null),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
<form [formGroup]="objectForm" (ngSubmit)="save()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||||
|
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<app-input-text i18n-title title="Username" formControlName="username" [error]="error?.username"></app-input-text>
|
||||||
|
<app-input-text i18n-title title="Email" formControlName="email" [error]="error?.email"></app-input-text>
|
||||||
|
<app-input-password i18n-title title="Password" formControlName="password" [error]="error?.password"></app-input-password>
|
||||||
|
<app-input-text i18n-title title="First name" formControlName="first_name" [error]="error?.first_name"></app-input-text>
|
||||||
|
<app-input-text i18n-title title="Last name" formControlName="last_name" [error]="error?.first_name"></app-input-text>
|
||||||
|
|
||||||
|
<div class="mb-2">
|
||||||
|
<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_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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-input-select i18n-title title="Groups" [items]="groups" multiple="true" formControlName="groups"></app-input-select>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<app-permissions-select i18n-title title="Permissions" formControlName="user_permissions" [error]="error?.user_permissions" [inheritedPermissions]="inheritedPermissions"></app-permissions-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
@ -0,0 +1,79 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core'
|
||||||
|
import { FormControl, FormGroup } from '@angular/forms'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { first } from 'rxjs'
|
||||||
|
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||||
|
import { PaperlessGroup } from 'src/app/data/paperless-group'
|
||||||
|
import { PaperlessUser } from 'src/app/data/paperless-user'
|
||||||
|
import { GroupService } from 'src/app/services/rest/group.service'
|
||||||
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-user-edit-dialog',
|
||||||
|
templateUrl: './user-edit-dialog.component.html',
|
||||||
|
styleUrls: ['./user-edit-dialog.component.scss'],
|
||||||
|
})
|
||||||
|
export class UserEditDialogComponent
|
||||||
|
extends EditDialogComponent<PaperlessUser>
|
||||||
|
implements OnInit
|
||||||
|
{
|
||||||
|
groups: PaperlessGroup[]
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
service: UserService,
|
||||||
|
activeModal: NgbActiveModal,
|
||||||
|
groupsService: GroupService
|
||||||
|
) {
|
||||||
|
super(service, activeModal, service)
|
||||||
|
|
||||||
|
groupsService
|
||||||
|
.listAll()
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe((result) => (this.groups = result.results))
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit()
|
||||||
|
this.onToggleSuperUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
getCreateTitle() {
|
||||||
|
return $localize`Create new user account`
|
||||||
|
}
|
||||||
|
|
||||||
|
getEditTitle() {
|
||||||
|
return $localize`Edit user account`
|
||||||
|
}
|
||||||
|
|
||||||
|
getForm(): FormGroup {
|
||||||
|
return new FormGroup({
|
||||||
|
username: new FormControl(''),
|
||||||
|
email: new FormControl(''),
|
||||||
|
password: new FormControl(null),
|
||||||
|
first_name: new FormControl(''),
|
||||||
|
last_name: new FormControl(''),
|
||||||
|
is_active: new FormControl(true),
|
||||||
|
is_superuser: new FormControl(false),
|
||||||
|
groups: new FormControl([]),
|
||||||
|
user_permissions: new FormControl([]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggleSuperUser() {
|
||||||
|
if (this.objectForm.get('is_superuser').value) {
|
||||||
|
this.objectForm.get('user_permissions').disable()
|
||||||
|
} else {
|
||||||
|
this.objectForm.get('user_permissions').enable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get inheritedPermissions(): string[] {
|
||||||
|
const groupsVal: Array<number> = this.objectForm.get('groups').value
|
||||||
|
|
||||||
|
if (!groupsVal) return []
|
||||||
|
else
|
||||||
|
return groupsVal.flatMap(
|
||||||
|
(id) => this.groups.find((g) => g.id == id)?.permissions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
<div class="btn-group w-100" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown">
|
<div class="btn-group w-100" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown">
|
||||||
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'">
|
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled">
|
||||||
<svg class="toolbaricon" fill="currentColor">
|
<svg class="toolbaricon" fill="currentColor">
|
||||||
<use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
|
<use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
|
||||||
</svg>
|
</svg>
|
||||||
@ -25,10 +25,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div *ngIf="selectionModel.items" class="items">
|
<div *ngIf="selectionModel.items" class="items">
|
||||||
<ng-container *ngFor="let item of selectionModel.itemsSorted | filter: filterText">
|
<ng-container *ngFor="let item of selectionModel.itemsSorted | filter: filterText">
|
||||||
<app-toggleable-dropdown-button *ngIf="allowSelectNone || item.id" [item]="item" [state]="selectionModel.get(item.id)" (toggle)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)"></app-toggleable-dropdown-button>
|
<app-toggleable-dropdown-button *ngIf="allowSelectNone || item.id" [item]="item" [state]="selectionModel.get(item.id)" (toggle)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)" [disabled]="disabled"></app-toggleable-dropdown-button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<button *ngIf="editing" class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!modelIsDirty">
|
<button *ngIf="editing" class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!modelIsDirty || disabled">
|
||||||
<small class="ms-2" [ngClass]="{'fw-bold': modelIsDirty}" i18n>Apply</small>
|
<small class="ms-2" [ngClass]="{'fw-bold': modelIsDirty}" i18n>Apply</small>
|
||||||
<svg width="1.5em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
<svg width="1.5em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-right" />
|
<use xlink:href="assets/bootstrap-icons.svg#arrow-right" />
|
||||||
|
@ -317,6 +317,9 @@ export class FilterableDropdownComponent {
|
|||||||
@Input()
|
@Input()
|
||||||
applyOnClose = false
|
applyOnClose = false
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
disabled = false
|
||||||
|
|
||||||
@Output()
|
@Output()
|
||||||
apply = new EventEmitter<ChangedItems>()
|
apply = new EventEmitter<ChangedItems>()
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="toggleItem($event)">
|
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="toggleItem($event)" [disabled]="disabled">
|
||||||
<div class="selected-icon me-1">
|
<div class="selected-icon me-1">
|
||||||
<ng-container *ngIf="isChecked()">
|
<ng-container *ngIf="isChecked()">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16">
|
||||||
|
@ -23,6 +23,9 @@ export class ToggleableDropdownButtonComponent {
|
|||||||
@Input()
|
@Input()
|
||||||
count: number
|
count: number
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
disabled: boolean = false
|
||||||
|
|
||||||
@Output()
|
@Output()
|
||||||
toggle = new EventEmitter()
|
toggle = new EventEmitter()
|
||||||
|
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
<div class="input-group" [class.is-invalid]="error">
|
<div class="input-group" [class.is-invalid]="error">
|
||||||
<input class="form-control" [class.is-invalid]="error" [placeholder]="placeholder" [id]="inputId" maxlength="10"
|
<input class="form-control" [class.is-invalid]="error" [placeholder]="placeholder" [id]="inputId" maxlength="10"
|
||||||
(dateSelect)="onChange(value)" (change)="onChange(value)" (keypress)="onKeyPress($event)" (paste)="onPaste($event)"
|
(dateSelect)="onChange(value)" (change)="onChange(value)" (keypress)="onKeyPress($event)" (paste)="onPaste($event)"
|
||||||
name="dp" [(ngModel)]="value" ngbDatepicker #datePicker="ngbDatepicker" #datePickerContent="ngModel">
|
name="dp" [(ngModel)]="value" ngbDatepicker #datePicker="ngbDatepicker" #datePickerContent="ngModel" [disabled]="disabled">
|
||||||
<button class="btn btn-outline-secondary calendar" (click)="datePicker.toggle()" type="button">
|
<button class="btn btn-outline-secondary calendar" (click)="datePicker.toggle()" type="button" [disabled]="disabled">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-calendar" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-calendar" viewBox="0 0 16 16">
|
||||||
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/>
|
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label" [for]="inputId">{{title}}</label>
|
<label class="form-label" [for]="inputId">{{title}}</label>
|
||||||
<div class="input-group" [class.is-invalid]="error">
|
<div class="input-group" [class.is-invalid]="error">
|
||||||
<input type="number" class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [class.is-invalid]="error">
|
<input type="number" class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [class.is-invalid]="error" [disabled]="disabled">
|
||||||
<button *ngIf="showAdd" class="btn btn-outline-secondary" type="button" id="button-addon1" (click)="nextAsn()" [disabled]="value">+1</button>
|
<button *ngIf="showAdd" class="btn btn-outline-secondary" type="button" id="button-addon1" (click)="nextAsn()" [disabled]="disabled">+1</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
{{error}}
|
{{error}}
|
||||||
|
@ -0,0 +1,68 @@
|
|||||||
|
<ng-container *ngIf="!accordion">
|
||||||
|
<h5 i18n>Permissions</h5>
|
||||||
|
<ng-container [ngTemplateOutlet]="permissionsForm"></ng-container>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="accordion">
|
||||||
|
<ngb-accordion #acc="ngbAccordion" activeIds="">
|
||||||
|
<ngb-panel i18n-title title="Edit Permissions">
|
||||||
|
<ng-template ngbPanelContent>
|
||||||
|
<ng-container [ngTemplateOutlet]="permissionsForm"></ng-container>
|
||||||
|
</ng-template>
|
||||||
|
</ngb-panel>
|
||||||
|
</ngb-accordion>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-template #permissionsForm>
|
||||||
|
<div [formGroup]="form">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-3">
|
||||||
|
<label class="form-label d-block my-2" i18n>Owner:</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-9">
|
||||||
|
<app-input-select [items]="users" bindLabel="username" formControlName="owner" [allowNull]="true"></app-input-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<small class="form-text text-muted text-end d-block mt-n2" i18n>Objects without an owner can be viewed and edited by all users</small>
|
||||||
|
<div formGroupName="set_permissions">
|
||||||
|
<h6 class="mt-3" i18n>View</h6>
|
||||||
|
<div formGroupName="view" class="mb-2">
|
||||||
|
<div class="row mb-1">
|
||||||
|
<div class="col-lg-3">
|
||||||
|
<label class="form-label d-block my-2" i18n>Users:</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-9">
|
||||||
|
<app-permissions-user type="view" formControlName="users"></app-permissions-user>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-3">
|
||||||
|
<label class="form-label d-block my-2" i18n>Groups:</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-9">
|
||||||
|
<app-permissions-group type="view" formControlName="groups"></app-permissions-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h6 class="mt-4" i18n>Edit</h6>
|
||||||
|
<div formGroupName="change">
|
||||||
|
<div class="row mb-1">
|
||||||
|
<div class="col-lg-3">
|
||||||
|
<label class="form-label d-block my-2" i18n>Users:</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-9">
|
||||||
|
<app-permissions-user type="change" formControlName="users"></app-permissions-user>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-3">
|
||||||
|
<label class="form-label d-block my-2" i18n>Groups:</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-9">
|
||||||
|
<app-permissions-group type="change" formControlName="groups"></app-permissions-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
@ -0,0 +1,69 @@
|
|||||||
|
import { Component, forwardRef, Input, OnInit } from '@angular/core'
|
||||||
|
import { FormControl, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||||
|
import { PaperlessUser } from 'src/app/data/paperless-user'
|
||||||
|
import { AbstractInputComponent } from '../../abstract-input'
|
||||||
|
|
||||||
|
export interface PermissionsFormObject {
|
||||||
|
owner?: number
|
||||||
|
set_permissions?: {
|
||||||
|
view?: {
|
||||||
|
users?: number[]
|
||||||
|
groups?: number[]
|
||||||
|
}
|
||||||
|
change?: {
|
||||||
|
users?: number[]
|
||||||
|
groups?: number[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
useExisting: forwardRef(() => PermissionsFormComponent),
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selector: 'app-permissions-form',
|
||||||
|
templateUrl: './permissions-form.component.html',
|
||||||
|
styleUrls: ['./permissions-form.component.scss'],
|
||||||
|
})
|
||||||
|
export class PermissionsFormComponent
|
||||||
|
extends AbstractInputComponent<PermissionsFormObject>
|
||||||
|
implements OnInit
|
||||||
|
{
|
||||||
|
@Input()
|
||||||
|
users: PaperlessUser[]
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
accordion: boolean = false
|
||||||
|
|
||||||
|
form = new FormGroup({
|
||||||
|
owner: new FormControl(null),
|
||||||
|
set_permissions: new FormGroup({
|
||||||
|
view: new FormGroup({
|
||||||
|
users: new FormControl([]),
|
||||||
|
groups: new FormControl([]),
|
||||||
|
}),
|
||||||
|
change: new FormGroup({
|
||||||
|
users: new FormControl([]),
|
||||||
|
groups: new FormControl([]),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.form.valueChanges.subscribe((value) => {
|
||||||
|
this.onChange(value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
writeValue(newValue: any): void {
|
||||||
|
this.form.patchValue(newValue, { emitEvent: false })
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
<div class="paperless-input-select">
|
||||||
|
<div>
|
||||||
|
<ng-select name="inputId" [(ngModel)]="value"
|
||||||
|
[disabled]="disabled"
|
||||||
|
clearable="true"
|
||||||
|
[items]="groups"
|
||||||
|
multiple="true"
|
||||||
|
bindLabel="name"
|
||||||
|
bindValue="id"
|
||||||
|
(change)="onChange(value)">
|
||||||
|
</ng-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,30 @@
|
|||||||
|
import { Component, forwardRef, Input, OnInit } from '@angular/core'
|
||||||
|
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||||
|
import { first } from 'rxjs/operators'
|
||||||
|
import { PaperlessGroup } from 'src/app/data/paperless-group'
|
||||||
|
import { GroupService } from 'src/app/services/rest/group.service'
|
||||||
|
import { AbstractInputComponent } from '../../abstract-input'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
useExisting: forwardRef(() => PermissionsGroupComponent),
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selector: 'app-permissions-group',
|
||||||
|
templateUrl: './permissions-group.component.html',
|
||||||
|
styleUrls: ['./permissions-group.component.scss'],
|
||||||
|
})
|
||||||
|
export class PermissionsGroupComponent extends AbstractInputComponent<PaperlessGroup> {
|
||||||
|
groups: PaperlessGroup[]
|
||||||
|
|
||||||
|
constructor(groupService: GroupService) {
|
||||||
|
super()
|
||||||
|
groupService
|
||||||
|
.listAll()
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe((result) => (this.groups = result.results))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
<div class="paperless-input-select">
|
||||||
|
<div>
|
||||||
|
<ng-select name="inputId" [(ngModel)]="value"
|
||||||
|
[disabled]="disabled"
|
||||||
|
clearable="true"
|
||||||
|
[items]="users"
|
||||||
|
multiple="true"
|
||||||
|
bindLabel="username"
|
||||||
|
bindValue="id"
|
||||||
|
(change)="onChange(value)">
|
||||||
|
</ng-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,38 @@
|
|||||||
|
import { Component, forwardRef, Input, OnInit } from '@angular/core'
|
||||||
|
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||||
|
import { first } from 'rxjs/operators'
|
||||||
|
import { PaperlessUser } from 'src/app/data/paperless-user'
|
||||||
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
import { AbstractInputComponent } from '../../abstract-input'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
useExisting: forwardRef(() => PermissionsUserComponent),
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selector: 'app-permissions-user',
|
||||||
|
templateUrl: './permissions-user.component.html',
|
||||||
|
styleUrls: ['./permissions-user.component.scss'],
|
||||||
|
})
|
||||||
|
export class PermissionsUserComponent extends AbstractInputComponent<
|
||||||
|
PaperlessUser[]
|
||||||
|
> {
|
||||||
|
users: PaperlessUser[]
|
||||||
|
|
||||||
|
constructor(userService: UserService, settings: SettingsService) {
|
||||||
|
super()
|
||||||
|
userService
|
||||||
|
.listAll()
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe(
|
||||||
|
(result) =>
|
||||||
|
(this.users = result.results.filter(
|
||||||
|
(u) => u.id !== settings.currentUser.id
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
<div class="mb-3 paperless-input-select">
|
<div class="mb-3 paperless-input-select" [class.disabled]="disabled">
|
||||||
<label class="form-label" [for]="inputId">{{title}}</label>
|
<label *ngIf="title" class="form-label" [for]="inputId">{{title}}</label>
|
||||||
<div [class.input-group]="allowCreateNew">
|
<div [class.input-group]="allowCreateNew">
|
||||||
<ng-select name="inputId" [(ngModel)]="value"
|
<ng-select name="inputId" [(ngModel)]="value"
|
||||||
[disabled]="disabled"
|
[disabled]="disabled"
|
||||||
@ -11,7 +11,8 @@
|
|||||||
addTagText="Add item"
|
addTagText="Add item"
|
||||||
i18n-addTagText="Used for both types, correspondents, storage paths"
|
i18n-addTagText="Used for both types, correspondents, storage paths"
|
||||||
[placeholder]="placeholder"
|
[placeholder]="placeholder"
|
||||||
bindLabel="name"
|
[multiple]="multiple"
|
||||||
|
[bindLabel]="bindLabel"
|
||||||
bindValue="id"
|
bindValue="id"
|
||||||
(change)="onChange(value)"
|
(change)="onChange(value)"
|
||||||
(search)="onSearch($event)"
|
(search)="onSearch($event)"
|
||||||
@ -19,7 +20,7 @@
|
|||||||
(clear)="clearLastSearchTerm()"
|
(clear)="clearLastSearchTerm()"
|
||||||
(blur)="onBlur()">
|
(blur)="onBlur()">
|
||||||
</ng-select>
|
</ng-select>
|
||||||
<button *ngIf="allowCreateNew" class="btn btn-outline-secondary" type="button" (click)="addItem()">
|
<button *ngIf="allowCreateNew" class="btn btn-outline-secondary" type="button" (click)="addItem()" [disabled]="disabled">
|
||||||
<svg class="buttonicon" fill="currentColor">
|
<svg class="buttonicon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#plus" />
|
<use xlink:href="assets/bootstrap-icons.svg#plus" />
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -1 +1,14 @@
|
|||||||
// styles for ng-select child are in styles.scss
|
// styles for ng-select child are in styles.scss
|
||||||
|
.paperless-input-select.disabled {
|
||||||
|
.input-group {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep ng-select {
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
.ng-select-container {
|
||||||
|
background-color: var(--pngx-bg-alt) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -44,6 +44,12 @@ export class SelectComponent extends AbstractInputComponent<number> {
|
|||||||
@Input()
|
@Input()
|
||||||
placeholder: string
|
placeholder: string
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
multiple: boolean = false
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
bindLabel: string = 'name'
|
||||||
|
|
||||||
@Output()
|
@Output()
|
||||||
createNew = new EventEmitter<string>()
|
createNew = new EventEmitter<string>()
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
<div class="mb-3 paperless-input-select paperless-input-tags">
|
<div class="mb-3 paperless-input-select paperless-input-tags" [class.disabled]="disabled">
|
||||||
<label class="form-label" for="tags" i18n>Tags</label>
|
<label class="form-label" for="tags" i18n>Tags</label>
|
||||||
|
|
||||||
<div class="input-group flex-nowrap">
|
<div class="input-group flex-nowrap">
|
||||||
<ng-select name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="value"
|
<ng-select name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="value"
|
||||||
|
[disabled]="disabled"
|
||||||
[multiple]="true"
|
[multiple]="true"
|
||||||
[closeOnSelect]="false"
|
[closeOnSelect]="false"
|
||||||
[clearSearchOnAdd]="true"
|
[clearSearchOnAdd]="true"
|
||||||
@ -31,7 +32,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-select>
|
</ng-select>
|
||||||
|
|
||||||
<button *ngIf="allowCreate" class="btn btn-outline-secondary" type="button" (click)="createTag()">
|
<button *ngIf="allowCreate" class="btn btn-outline-secondary" type="button" (click)="createTag()" [disabled]="disabled">
|
||||||
<svg class="buttonicon" fill="currentColor">
|
<svg class="buttonicon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#plus" />
|
<use xlink:href="assets/bootstrap-icons.svg#plus" />
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -10,3 +10,17 @@
|
|||||||
.tag-wrap-delete {
|
.tag-wrap-delete {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.paperless-input-select.disabled {
|
||||||
|
.input-group {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep ng-select {
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
.ng-select-container {
|
||||||
|
background-color: var(--pngx-bg-alt) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -74,6 +74,8 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
removeTag(event: PointerEvent, id: number) {
|
removeTag(event: PointerEvent, id: number) {
|
||||||
|
if (this.disabled) return
|
||||||
|
|
||||||
// prevent opening dropdown
|
// prevent opening dropdown
|
||||||
event.stopImmediatePropagation()
|
event.stopImmediatePropagation()
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label" [for]="inputId">{{title}}</label>
|
<label class="form-label" [for]="inputId">{{title}}</label>
|
||||||
<input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)">
|
<input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled">
|
||||||
<small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
|
<small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
{{error}}
|
{{error}}
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
|
||||||
|
<button type="button" class="btn-close" aria-label="Close" (click)="cancelClicked()">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<p class="mb-3" *ngIf="message" [innerHTML]="message | safeHtml"></p>
|
||||||
|
|
||||||
|
<form [formGroup]="form">
|
||||||
|
<app-permissions-form [users]="users" formControlName="permissions_form"></app-permissions-form>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-primary" (click)="cancelClicked()" i18n>Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary" (click)="confirmClicked.emit(permissions)" i18n>Confirm</button>
|
||||||
|
</div>
|
@ -0,0 +1,46 @@
|
|||||||
|
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||||
|
import { FormControl, FormGroup } from '@angular/forms'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { PaperlessUser } from 'src/app/data/paperless-user'
|
||||||
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-permissions-dialog',
|
||||||
|
templateUrl: './permissions-dialog.component.html',
|
||||||
|
styleUrls: ['./permissions-dialog.component.scss'],
|
||||||
|
})
|
||||||
|
export class PermissionsDialogComponent {
|
||||||
|
users: PaperlessUser[]
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public activeModal: NgbActiveModal,
|
||||||
|
private userService: UserService
|
||||||
|
) {
|
||||||
|
this.userService.listAll().subscribe((r) => (this.users = r.results))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
public confirmClicked = new EventEmitter()
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
title = $localize`Set Permissions`
|
||||||
|
|
||||||
|
form = new FormGroup({
|
||||||
|
permissions_form: new FormControl(),
|
||||||
|
})
|
||||||
|
|
||||||
|
get permissions() {
|
||||||
|
return {
|
||||||
|
owner: this.form.get('permissions_form').value?.owner ?? null,
|
||||||
|
set_permissions:
|
||||||
|
this.form.get('permissions_form').value?.set_permissions ?? null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
message = $localize`Note that permissions set here will override any existing permissions`
|
||||||
|
|
||||||
|
cancelClicked() {
|
||||||
|
this.activeModal.close()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
<form [formGroup]="form" [class.opacity-50]="disabled">
|
||||||
|
<label class="form-label">{{title}}</label>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item d-flex">
|
||||||
|
<div class="col-3" i18n>Type</div>
|
||||||
|
<div class="col" i18n>All</div>
|
||||||
|
<div class="col" i18n>Add</div>
|
||||||
|
<div class="col" i18n>Change</div>
|
||||||
|
<div class="col" i18n>Delete</div>
|
||||||
|
<div class="col" i18n>View</div>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex" *ngFor="let type of PermissionType | keyvalue" [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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngFor="let action of PermissionAction | keyvalue" 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}}" [attr.disabled]="isDisabled(type.key, action.key)">
|
||||||
|
<label class="form-check-label visually-hidden" for="{{type.key}}_{{action.key}}" i18n>{{action.key}}</label>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<div *ngIf="error" class="invalid-feedback d-block">{{error}}</div>
|
||||||
|
</ul>
|
||||||
|
</form>
|
@ -0,0 +1,189 @@
|
|||||||
|
import { Component, forwardRef, Input, OnInit } from '@angular/core'
|
||||||
|
import {
|
||||||
|
ControlValueAccessor,
|
||||||
|
FormControl,
|
||||||
|
FormGroup,
|
||||||
|
NG_VALUE_ACCESSOR,
|
||||||
|
} from '@angular/forms'
|
||||||
|
import {
|
||||||
|
PermissionAction,
|
||||||
|
PermissionsService,
|
||||||
|
PermissionType,
|
||||||
|
} from 'src/app/services/permissions.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
useExisting: forwardRef(() => PermissionsSelectComponent),
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selector: 'app-permissions-select',
|
||||||
|
templateUrl: './permissions-select.component.html',
|
||||||
|
styleUrls: ['./permissions-select.component.scss'],
|
||||||
|
})
|
||||||
|
export class PermissionsSelectComponent
|
||||||
|
implements OnInit, ControlValueAccessor
|
||||||
|
{
|
||||||
|
PermissionType = PermissionType
|
||||||
|
PermissionAction = PermissionAction
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
title: string = 'Permissions'
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
error: string
|
||||||
|
|
||||||
|
permissions: string[]
|
||||||
|
|
||||||
|
form = new FormGroup({})
|
||||||
|
|
||||||
|
typesWithAllActions: Set<string> = new Set()
|
||||||
|
|
||||||
|
_inheritedPermissions: string[] = []
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set inheritedPermissions(inherited: string[]) {
|
||||||
|
// remove <app_label>. from permission strings
|
||||||
|
const newInheritedPermissions = inherited?.length
|
||||||
|
? inherited.map((p) => p.replace(/^\w+\./g, ''))
|
||||||
|
: []
|
||||||
|
|
||||||
|
if (this._inheritedPermissions !== newInheritedPermissions) {
|
||||||
|
this._inheritedPermissions = newInheritedPermissions
|
||||||
|
this.writeValue(this.permissions) // updates visual checks etc.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inheritedWarning: string = $localize`Inerhited from group`
|
||||||
|
|
||||||
|
constructor(private readonly permissionsService: PermissionsService) {
|
||||||
|
for (const type in PermissionType) {
|
||||||
|
const control = new FormGroup({})
|
||||||
|
for (const action in PermissionAction) {
|
||||||
|
control.addControl(action, new FormControl(null))
|
||||||
|
}
|
||||||
|
this.form.addControl(type, control)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeValue(permissions: string[]): void {
|
||||||
|
this.permissions = permissions ?? []
|
||||||
|
const allPerms = this._inheritedPermissions.concat(this.permissions)
|
||||||
|
|
||||||
|
allPerms.forEach((permissionStr) => {
|
||||||
|
const { actionKey, typeKey } =
|
||||||
|
this.permissionsService.getPermissionKeys(permissionStr)
|
||||||
|
|
||||||
|
if (actionKey && typeKey) {
|
||||||
|
if (this.form.get(typeKey)?.get(actionKey)) {
|
||||||
|
this.form
|
||||||
|
.get(typeKey)
|
||||||
|
.get(actionKey)
|
||||||
|
.patchValue(true, { emitEvent: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Object.keys(PermissionType).forEach((type) => {
|
||||||
|
if (
|
||||||
|
Object.values(this.form.get(type).value).every((val) => val == true)
|
||||||
|
) {
|
||||||
|
this.typesWithAllActions.add(type)
|
||||||
|
} else {
|
||||||
|
this.typesWithAllActions.delete(type)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange = (newValue: string[]) => {}
|
||||||
|
|
||||||
|
onTouched = () => {}
|
||||||
|
|
||||||
|
disabled: boolean = false
|
||||||
|
|
||||||
|
registerOnChange(fn: any): void {
|
||||||
|
this.onChange = fn
|
||||||
|
}
|
||||||
|
|
||||||
|
registerOnTouched(fn: any): void {
|
||||||
|
this.onTouched = fn
|
||||||
|
}
|
||||||
|
|
||||||
|
setDisabledState?(isDisabled: boolean): void {
|
||||||
|
this.disabled = isDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.form.valueChanges.subscribe((newValue) => {
|
||||||
|
let permissions = []
|
||||||
|
Object.entries(newValue).forEach(([typeKey, typeValue]) => {
|
||||||
|
// e.g. [Document, { Add: true, View: true ... }]
|
||||||
|
const selectedActions = Object.entries(typeValue).filter(
|
||||||
|
([actionKey, actionValue]) => actionValue == true
|
||||||
|
)
|
||||||
|
|
||||||
|
selectedActions.forEach(([actionKey, actionValue]) => {
|
||||||
|
permissions.push(
|
||||||
|
(PermissionType[typeKey] as string).replace(
|
||||||
|
'%s',
|
||||||
|
PermissionAction[actionKey]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (selectedActions.length == Object.entries(typeValue).length) {
|
||||||
|
this.typesWithAllActions.add(typeKey)
|
||||||
|
} else {
|
||||||
|
this.typesWithAllActions.delete(typeKey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.onChange(permissions)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleAll(event, type) {
|
||||||
|
const typeGroup = this.form.get(type)
|
||||||
|
if (event.target.checked) {
|
||||||
|
Object.keys(PermissionAction).forEach((action) => {
|
||||||
|
typeGroup.get(action).patchValue(true)
|
||||||
|
})
|
||||||
|
this.typesWithAllActions.add(type)
|
||||||
|
} else {
|
||||||
|
Object.keys(PermissionAction).forEach((action) => {
|
||||||
|
typeGroup.get(action).patchValue(false)
|
||||||
|
})
|
||||||
|
this.typesWithAllActions.delete(type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isInherited(typeKey: string, actionKey: string = null) {
|
||||||
|
if (this._inheritedPermissions.length == 0) return false
|
||||||
|
else if (actionKey) {
|
||||||
|
return this._inheritedPermissions.includes(
|
||||||
|
this.permissionsService.getPermissionCode(
|
||||||
|
PermissionAction[actionKey],
|
||||||
|
PermissionType[typeKey]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return Object.values(PermissionAction).every((action) => {
|
||||||
|
return this._inheritedPermissions.includes(
|
||||||
|
this.permissionsService.getPermissionCode(
|
||||||
|
action as PermissionAction,
|
||||||
|
PermissionType[typeKey]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if checkbox is disabled either because "All", inhereted or entire component disabled
|
||||||
|
isDisabled(typeKey: string, actionKey: string) {
|
||||||
|
return this.typesWithAllActions.has(typeKey) ||
|
||||||
|
this.isInherited(typeKey, actionKey) ||
|
||||||
|
this.disabled
|
||||||
|
? true
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
}
|
@ -9,3 +9,14 @@
|
|||||||
.toast:not(.show) {
|
.toast:not(.show) {
|
||||||
display: block; // this corrects an ng-bootstrap bug that prevented animations
|
display: block; // this corrects an ng-bootstrap bug that prevented animations
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::ng-deep .toast.error .toast-header {
|
||||||
|
background-color: hsla(350, 79%, 40%, 0.8); // bg-danger
|
||||||
|
border-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .toast.error .toast-body {
|
||||||
|
background-color: hsla(350, 79%, 40%, 0.8); // bg-danger
|
||||||
|
border-bottom-left-radius: inherit;
|
||||||
|
border-bottom-right-radius: inherit;
|
||||||
|
}
|
||||||
|
@ -28,12 +28,14 @@
|
|||||||
|
|
||||||
<app-welcome-widget *ngIf="settingsService.offerTour()" tourAnchor="tour.dashboard"></app-welcome-widget>
|
<app-welcome-widget *ngIf="settingsService.offerTour()" tourAnchor="tour.dashboard"></app-welcome-widget>
|
||||||
|
|
||||||
<ng-container *ngFor="let v of savedViewService.dashboardViews; first as isFirst">
|
<div *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
|
||||||
<app-saved-view-widget *ngIf="isFirst; else noTour" [savedView]="v" tourAnchor="tour.dashboard"></app-saved-view-widget>
|
<ng-container *ngFor="let v of savedViewService.dashboardViews; first as isFirst">
|
||||||
<ng-template #noTour>
|
<app-saved-view-widget *ngIf="isFirst; else noTour" [savedView]="v" tourAnchor="tour.dashboard"></app-saved-view-widget>
|
||||||
<app-saved-view-widget [savedView]="v"></app-saved-view-widget>
|
<ng-template #noTour>
|
||||||
</ng-template>
|
<app-saved-view-widget [savedView]="v"></app-saved-view-widget>
|
||||||
</ng-container>
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-dashboard',
|
selector: 'app-dashboard',
|
||||||
templateUrl: './dashboard.component.html',
|
templateUrl: './dashboard.component.html',
|
||||||
styleUrls: ['./dashboard.component.scss'],
|
styleUrls: ['./dashboard.component.scss'],
|
||||||
})
|
})
|
||||||
export class DashboardComponent {
|
export class DashboardComponent extends ComponentWithPermissions {
|
||||||
constructor(
|
constructor(
|
||||||
public savedViewService: SavedViewService,
|
public savedViewService: SavedViewService,
|
||||||
public settingsService: SettingsService
|
public settingsService: SettingsService
|
||||||
) {}
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
get subtitle() {
|
get subtitle() {
|
||||||
if (this.settingsService.displayName) {
|
if (this.settingsService.displayName) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<app-widget-frame [title]="savedView.name" [loading]="loading">
|
<app-widget-frame [title]="savedView.name" [loading]="loading">
|
||||||
|
|
||||||
<a class="btn-link" header-buttons [routerLink]="[]" (click)="showAll()" i18n>Show all</a>
|
<a class="btn-link" header-buttons [routerLink]="[]" (click)="showAll()" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }" i18n>Show all</a>
|
||||||
|
|
||||||
|
|
||||||
<table content class="table table-sm table-hover table-borderless mb-0">
|
<table content class="table table-sm table-hover table-borderless mb-0">
|
||||||
@ -10,7 +10,7 @@
|
|||||||
<th scope="col" i18n>Title</th>
|
<th scope="col" i18n>Title</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||||
<tr *ngFor="let doc of documents">
|
<tr *ngFor="let doc of documents">
|
||||||
<td><a routerLink="/documents/{{doc.id}}" class="d-block text-dark text-decoration-none">{{doc.created_date | customDate}}</a></td>
|
<td><a routerLink="/documents/{{doc.id}}" class="d-block text-dark text-decoration-none">{{doc.created_date | customDate}}</a></td>
|
||||||
<td><a routerLink="/documents/{{doc.id}}" class="d-block text-dark text-decoration-none">{{doc.title | documentTitle}}<app-tag [tag]="t" *ngFor="let t of doc.tags$ | async" class="ms-1" (click)="clickTag(t, $event)"></app-tag></a></td>
|
<td><a routerLink="/documents/{{doc.id}}" class="d-block text-dark text-decoration-none">{{doc.title | documentTitle}}<app-tag [tag]="t" *ngFor="let t of doc.tags$ | async" class="ms-1" (click)="clickTag(t, $event)"></app-tag></a></td>
|
||||||
|
@ -9,13 +9,17 @@ import { PaperlessTag } from 'src/app/data/paperless-tag'
|
|||||||
import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type'
|
import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type'
|
||||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||||
|
import { ComponentWithPermissions } from 'src/app/components/with-permissions/with-permissions.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-saved-view-widget',
|
selector: 'app-saved-view-widget',
|
||||||
templateUrl: './saved-view-widget.component.html',
|
templateUrl: './saved-view-widget.component.html',
|
||||||
styleUrls: ['./saved-view-widget.component.scss'],
|
styleUrls: ['./saved-view-widget.component.scss'],
|
||||||
})
|
})
|
||||||
export class SavedViewWidgetComponent implements OnInit, OnDestroy {
|
export class SavedViewWidgetComponent
|
||||||
|
extends ComponentWithPermissions
|
||||||
|
implements OnInit, OnDestroy
|
||||||
|
{
|
||||||
loading: boolean = true
|
loading: boolean = true
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -24,7 +28,9 @@ export class SavedViewWidgetComponent implements OnInit, OnDestroy {
|
|||||||
private list: DocumentListViewService,
|
private list: DocumentListViewService,
|
||||||
private consumerStatusService: ConsumerStatusService,
|
private consumerStatusService: ConsumerStatusService,
|
||||||
public openDocumentsService: OpenDocumentsService
|
public openDocumentsService: OpenDocumentsService
|
||||||
) {}
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
savedView: PaperlessSavedView
|
savedView: PaperlessSavedView
|
||||||
@ -74,6 +80,7 @@ export class SavedViewWidgetComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
clickTag(tag: PaperlessTag, event: MouseEvent) {
|
clickTag(tag: PaperlessTag, event: MouseEvent) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
event.stopImmediatePropagation()
|
||||||
|
|
||||||
this.list.quickFilter([
|
this.list.quickFilter([
|
||||||
{ rule_type: FILTER_HAS_TAGS_ALL, value: tag.id.toString() },
|
{ rule_type: FILTER_HAS_TAGS_ALL, value: tag.id.toString() },
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div content tourAnchor="tour.upload-widget">
|
<div content tourAnchor="tour.upload-widget">
|
||||||
<form>
|
<form *appIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Document }">
|
||||||
<ngx-file-drop dropZoneLabel="Drop documents here or" browseBtnLabel="Browse files" (onFileDrop)="dropped($event)"
|
<ngx-file-drop dropZoneLabel="Drop documents here or" browseBtnLabel="Browse files" (onFileDrop)="dropped($event)"
|
||||||
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" dropZoneClassName="bg-light card"
|
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" dropZoneClassName="bg-light card"
|
||||||
multiple="true" contentClassName="justify-content-center d-flex align-items-center py-5 px-2" [showBrowseBtn]=true
|
multiple="true" contentClassName="justify-content-center d-flex align-items-center py-5 px-2" [showBrowseBtn]=true
|
||||||
@ -40,13 +40,15 @@
|
|||||||
<h6 class="alert-heading">{{status.filename}}</h6>
|
<h6 class="alert-heading">{{status.filename}}</h6>
|
||||||
<p class="mb-0 pb-1" *ngIf="!isFinished(status) || (isFinished(status) && !status.documentId)">{{status.message}}</p>
|
<p class="mb-0 pb-1" *ngIf="!isFinished(status) || (isFinished(status) && !status.documentId)">{{status.message}}</p>
|
||||||
<ngb-progressbar [value]="status.getProgress()" [max]="1" [type]="getStatusColor(status)"></ngb-progressbar>
|
<ngb-progressbar [value]="status.getProgress()" [max]="1" [type]="getStatusColor(status)"></ngb-progressbar>
|
||||||
<div *ngIf="isFinished(status)">
|
<div *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||||
<button *ngIf="status.documentId" class="btn btn-sm btn-outline-primary btn-open" routerLink="/documents/{{status.documentId}}" (click)="dismiss(status)">
|
<div *ngIf="isFinished(status)">
|
||||||
<small i18n>Open document</small>
|
<button *ngIf="status.documentId" class="btn btn-sm btn-outline-primary btn-open" routerLink="/documents/{{status.documentId}}" (click)="dismiss(status)">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" fill="currentColor" class="bi bi-arrow-right-short" viewBox="0 0 16 16">
|
<small i18n>Open document</small>
|
||||||
<path fill-rule="evenodd" d="M4 8a.5.5 0 0 1 .5-.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5A.5.5 0 0 1 4 8z"/>
|
<svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" fill="currentColor" class="bi bi-arrow-right-short" viewBox="0 0 16 16">
|
||||||
</svg>
|
<path fill-rule="evenodd" d="M4 8a.5.5 0 0 1 .5-.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5A.5.5 0 0 1 4 8z"/>
|
||||||
</button>
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ngb-alert>
|
</ngb-alert>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { NgxFileDropEntry } from 'ngx-file-drop'
|
import { NgxFileDropEntry } from 'ngx-file-drop'
|
||||||
|
import { ComponentWithPermissions } from 'src/app/components/with-permissions/with-permissions.component'
|
||||||
import {
|
import {
|
||||||
ConsumerStatusService,
|
ConsumerStatusService,
|
||||||
FileStatus,
|
FileStatus,
|
||||||
@ -14,13 +15,15 @@ const MAX_ALERTS = 5
|
|||||||
templateUrl: './upload-file-widget.component.html',
|
templateUrl: './upload-file-widget.component.html',
|
||||||
styleUrls: ['./upload-file-widget.component.scss'],
|
styleUrls: ['./upload-file-widget.component.scss'],
|
||||||
})
|
})
|
||||||
export class UploadFileWidgetComponent {
|
export class UploadFileWidgetComponent extends ComponentWithPermissions {
|
||||||
alertsExpanded = false
|
alertsExpanded = false
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private consumerStatusService: ConsumerStatusService,
|
private consumerStatusService: ConsumerStatusService,
|
||||||
private uploadDocumentsService: UploadDocumentsService
|
private uploadDocumentsService: UploadDocumentsService
|
||||||
) {}
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
getStatus() {
|
getStatus() {
|
||||||
return this.consumerStatusService.getConsumerStatus().slice(0, MAX_ALERTS)
|
return this.consumerStatusService.getConsumerStatus().slice(0, MAX_ALERTS)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<div *ngIf="comments">
|
<div *ngIf="comments">
|
||||||
<form [formGroup]="commentForm" class="needs-validation mt-3" novalidate>
|
<form [formGroup]="commentForm" class="needs-validation mt-3" *appIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Comment }" novalidate>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<textarea class="form-control form-control-sm" [class.is-invalid]="newCommentError" rows="3" formControlName="newComment" placeholder="Enter comment" i18n-placeholder required></textarea>
|
<textarea class="form-control form-control-sm" [class.is-invalid]="newCommentError" rows="3" formControlName="newComment" placeholder="Enter comment" i18n-placeholder required></textarea>
|
||||||
<div class="invalid-feedback" i18n>
|
<div class="invalid-feedback" i18n>
|
||||||
@ -18,7 +18,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="d-flex card-footer small bg-light text-primary justify-content-between align-items-center">
|
<div class="d-flex card-footer small bg-light text-primary justify-content-between align-items-center">
|
||||||
<span>{{displayName(comment)}} - {{ comment.created | customDate}}</span>
|
<span>{{displayName(comment)}} - {{ comment.created | customDate}}</span>
|
||||||
<button type="button" class="btn btn-link btn-sm p-0 fade" (click)="deleteComment(comment.id)">
|
<button type="button" class="btn btn-link btn-sm p-0 fade" (click)="deleteComment(comment.id)" *appIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Comment }">
|
||||||
<svg width="13" height="13" fill="currentColor">
|
<svg width="13" height="13" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -4,13 +4,14 @@ import { PaperlessDocumentComment } from 'src/app/data/paperless-document-commen
|
|||||||
import { FormControl, FormGroup } from '@angular/forms'
|
import { FormControl, FormGroup } from '@angular/forms'
|
||||||
import { first } from 'rxjs/operators'
|
import { first } from 'rxjs/operators'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-document-comments',
|
selector: 'app-document-comments',
|
||||||
templateUrl: './document-comments.component.html',
|
templateUrl: './document-comments.component.html',
|
||||||
styleUrls: ['./document-comments.component.scss'],
|
styleUrls: ['./document-comments.component.scss'],
|
||||||
})
|
})
|
||||||
export class DocumentCommentsComponent {
|
export class DocumentCommentsComponent extends ComponentWithPermissions {
|
||||||
commentForm: FormGroup = new FormGroup({
|
commentForm: FormGroup = new FormGroup({
|
||||||
newComment: new FormControl(''),
|
newComment: new FormControl(''),
|
||||||
})
|
})
|
||||||
@ -32,7 +33,9 @@ export class DocumentCommentsComponent {
|
|||||||
constructor(
|
constructor(
|
||||||
private commentsService: DocumentCommentsService,
|
private commentsService: DocumentCommentsService,
|
||||||
private toastService: ToastService
|
private toastService: ToastService
|
||||||
) {}
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
update(): void {
|
update(): void {
|
||||||
this.networkActive = true
|
this.networkActive = true
|
||||||
@ -89,8 +92,8 @@ export class DocumentCommentsComponent {
|
|||||||
displayName(comment: PaperlessDocumentComment): string {
|
displayName(comment: PaperlessDocumentComment): string {
|
||||||
if (!comment.user) return ''
|
if (!comment.user) return ''
|
||||||
let nameComponents = []
|
let nameComponents = []
|
||||||
if (comment.user.firstname) nameComponents.unshift(comment.user.firstname)
|
if (comment.user.first_name) nameComponents.unshift(comment.user.first_name)
|
||||||
if (comment.user.lastname) nameComponents.unshift(comment.user.lastname)
|
if (comment.user.last_name) nameComponents.unshift(comment.user.last_name)
|
||||||
if (comment.user.username) {
|
if (comment.user.username) {
|
||||||
if (nameComponents.length > 0)
|
if (nameComponents.length > 0)
|
||||||
nameComponents.push(`(${comment.user.username})`)
|
nameComponents.push(`(${comment.user.username})`)
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<div class="input-group-text" i18n>of {{previewNumPages}}</div>
|
<div class="input-group-text" i18n>of {{previewNumPages}}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger me-2 ms-auto" (click)="delete()">
|
<button type="button" class="btn btn-sm btn-outline-danger me-2 ms-auto" (click)="delete()" [disabled]="!userIsOwner">
|
||||||
<svg class="buttonicon" fill="currentColor">
|
<svg class="buttonicon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||||
</svg><span class="d-none d-lg-inline ps-1" i18n>Delete</span>
|
</svg><span class="d-none d-lg-inline ps-1" i18n>Delete</span>
|
||||||
@ -20,7 +20,7 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="btn-group" ngbDropdown role="group" *ngIf="metadata?.has_archive_version">
|
<div class="btn-group" ngbDropdown role="group" *ngIf="metadata?.has_archive_version">
|
||||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle-split" ngbDropdownToggle></button>
|
<button class="btn btn-sm btn-outline-primary dropdown-toggle" ngbDropdownToggle></button>
|
||||||
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
||||||
<a ngbDropdownItem [href]="downloadOriginalUrl" i18n>Download original</a>
|
<a ngbDropdownItem [href]="downloadOriginalUrl" i18n>Download original</a>
|
||||||
</div>
|
</div>
|
||||||
@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="redoOcr()">
|
<button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="redoOcr()" [disabled]="!userCanEdit">
|
||||||
<svg class="buttonicon" fill="currentColor">
|
<svg class="buttonicon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-counterclockwise" />
|
<use xlink:href="assets/bootstrap-icons.svg#arrow-counterclockwise" />
|
||||||
</svg><span class="d-none d-lg-inline ps-1" i18n>Redo OCR</span>
|
</svg><span class="d-none d-lg-inline ps-1" i18n>Redo OCR</span>
|
||||||
@ -148,7 +148,7 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li [ngbNavItem]="4" class="d-md-none">
|
<li [ngbNavItem]="4" class="d-md-none">
|
||||||
<a ngbNavLink>Preview</a>
|
<a ngbNavLink i18n>Preview</a>
|
||||||
<ng-template ngbNavContent *ngIf="!pdfPreview.offsetParent">
|
<ng-template ngbNavContent *ngIf="!pdfPreview.offsetParent">
|
||||||
<div class="position-relative">
|
<div class="position-relative">
|
||||||
<ng-container *ngIf="getContentType() === 'application/pdf'">
|
<ng-container *ngIf="getContentType() === 'application/pdf'">
|
||||||
@ -170,19 +170,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li [ngbNavItem]="5" *ngIf="commentsEnabled">
|
<li [ngbNavItem]="5" *ngIf="commentsEnabled">
|
||||||
<a ngbNavLink i18n>Comments</a>
|
<a ngbNavLink i18n>Comments</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<app-document-comments [documentId]="documentId"></app-document-comments>
|
<app-document-comments [documentId]="documentId"></app-document-comments>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li [ngbNavItem]="6" *appIfOwner="document">
|
||||||
|
<a ngbNavLink i18n>Permissions</a>
|
||||||
|
<ng-template ngbNavContent>
|
||||||
|
<div class="mb-3">
|
||||||
|
<app-permissions-form [users]="users" formControlName="permissions_form"></app-permissions-form>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div [ngbNavOutlet]="nav" class="mt-2"></div>
|
<div [ngbNavOutlet]="nav" class="mt-2"></div>
|
||||||
|
|
||||||
<button type="button" class="btn btn-outline-secondary" (click)="discard()" i18n [disabled]="networkActive || (isDirty$ | async) !== true">Discard</button>
|
<ng-container>
|
||||||
<button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()" i18n [disabled]="networkActive || (isDirty$ | async) !== true || error">Save & next</button>
|
<button type="button" class="btn btn-outline-secondary" (click)="discard()" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Discard</button>
|
||||||
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive || (isDirty$ | async) !== true || error">Save</button>
|
<button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true || error">Save & next</button>
|
||||||
|
<button type="submit" class="btn btn-primary" *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true || error">Save</button>
|
||||||
|
</ng-container>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -35,6 +35,13 @@ import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
|||||||
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
||||||
import { StoragePathEditDialogComponent } from '../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
import { StoragePathEditDialogComponent } from '../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||||
|
import {
|
||||||
|
PermissionAction,
|
||||||
|
PermissionsService,
|
||||||
|
PermissionType,
|
||||||
|
} from 'src/app/services/permissions.service'
|
||||||
|
import { PaperlessUser } from 'src/app/data/paperless-user'
|
||||||
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-document-detail',
|
selector: 'app-document-detail',
|
||||||
@ -58,6 +65,7 @@ export class DocumentDetailComponent
|
|||||||
document: PaperlessDocument
|
document: PaperlessDocument
|
||||||
metadata: PaperlessDocumentMetadata
|
metadata: PaperlessDocumentMetadata
|
||||||
suggestions: PaperlessDocumentSuggestions
|
suggestions: PaperlessDocumentSuggestions
|
||||||
|
users: PaperlessUser[]
|
||||||
|
|
||||||
title: string
|
title: string
|
||||||
titleSubject: Subject<string> = new Subject()
|
titleSubject: Subject<string> = new Subject()
|
||||||
@ -78,6 +86,7 @@ export class DocumentDetailComponent
|
|||||||
storage_path: new FormControl(),
|
storage_path: new FormControl(),
|
||||||
archive_serial_number: new FormControl(),
|
archive_serial_number: new FormControl(),
|
||||||
tags: new FormControl([]),
|
tags: new FormControl([]),
|
||||||
|
permissions_form: new FormControl(null),
|
||||||
})
|
})
|
||||||
|
|
||||||
previewCurrentPage: number = 1
|
previewCurrentPage: number = 1
|
||||||
@ -106,6 +115,9 @@ export class DocumentDetailComponent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PermissionAction = PermissionAction
|
||||||
|
PermissionType = PermissionType
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private documentsService: DocumentService,
|
private documentsService: DocumentService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
@ -118,7 +130,9 @@ export class DocumentDetailComponent
|
|||||||
private documentTitlePipe: DocumentTitlePipe,
|
private documentTitlePipe: DocumentTitlePipe,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private settings: SettingsService,
|
private settings: SettingsService,
|
||||||
private storagePathService: StoragePathService
|
private storagePathService: StoragePathService,
|
||||||
|
private permissionsService: PermissionsService,
|
||||||
|
private userService: UserService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
titleKeyUp(event) {
|
titleKeyUp(event) {
|
||||||
@ -147,7 +161,13 @@ export class DocumentDetailComponent
|
|||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
this.error = null
|
this.error = null
|
||||||
Object.assign(this.document, this.documentForm.value)
|
const docValues = Object.assign({}, this.documentForm.value)
|
||||||
|
docValues['owner'] =
|
||||||
|
this.documentForm.get('permissions_form').value['owner']
|
||||||
|
docValues['set_permissions'] =
|
||||||
|
this.documentForm.get('permissions_form').value['set_permissions']
|
||||||
|
delete docValues['permissions_form']
|
||||||
|
Object.assign(this.document, docValues)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.correspondentService
|
this.correspondentService
|
||||||
@ -165,6 +185,11 @@ export class DocumentDetailComponent
|
|||||||
.pipe(first())
|
.pipe(first())
|
||||||
.subscribe((result) => (this.storagePaths = result.results))
|
.subscribe((result) => (this.storagePaths = result.results))
|
||||||
|
|
||||||
|
this.userService
|
||||||
|
.listAll()
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe((result) => (this.users = result.results))
|
||||||
|
|
||||||
this.route.paramMap
|
this.route.paramMap
|
||||||
.pipe(
|
.pipe(
|
||||||
takeUntil(this.unsubscribeNotifier),
|
takeUntil(this.unsubscribeNotifier),
|
||||||
@ -232,6 +257,10 @@ export class DocumentDetailComponent
|
|||||||
storage_path: doc.storage_path,
|
storage_path: doc.storage_path,
|
||||||
archive_serial_number: doc.archive_serial_number,
|
archive_serial_number: doc.archive_serial_number,
|
||||||
tags: [...doc.tags],
|
tags: [...doc.tags],
|
||||||
|
permissions_form: {
|
||||||
|
owner: doc.owner,
|
||||||
|
set_permissions: doc.permissions,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
this.isDirty$ = dirtyCheck(
|
this.isDirty$ = dirtyCheck(
|
||||||
@ -272,6 +301,9 @@ export class DocumentDetailComponent
|
|||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
this.metadata = null
|
this.metadata = null
|
||||||
|
this.toastService.showError(
|
||||||
|
$localize`Error retrieving metadata` + ': ' + error.toString()
|
||||||
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
this.documentsService
|
this.documentsService
|
||||||
@ -283,10 +315,20 @@ export class DocumentDetailComponent
|
|||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
this.suggestions = null
|
this.suggestions = null
|
||||||
|
this.toastService.showError(
|
||||||
|
$localize`Error retrieving suggestions` + ': ' + error.toString()
|
||||||
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
this.title = this.documentTitlePipe.transform(doc.title)
|
this.title = this.documentTitlePipe.transform(doc.title)
|
||||||
this.documentForm.patchValue(doc)
|
const docFormValues = Object.assign({}, doc)
|
||||||
|
docFormValues['permissions_form'] = {
|
||||||
|
owner: doc.owner,
|
||||||
|
set_permissions: doc.permissions,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.documentForm.patchValue(docFormValues, { emitEvent: false })
|
||||||
|
if (!this.userCanEdit) this.documentForm.disable()
|
||||||
}
|
}
|
||||||
|
|
||||||
createDocumentType(newName: string) {
|
createDocumentType(newName: string) {
|
||||||
@ -378,7 +420,7 @@ export class DocumentDetailComponent
|
|||||||
.update(this.document)
|
.update(this.document)
|
||||||
.pipe(first())
|
.pipe(first())
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (result) => {
|
next: () => {
|
||||||
this.close()
|
this.close()
|
||||||
this.networkActive = false
|
this.networkActive = false
|
||||||
this.error = null
|
this.error = null
|
||||||
@ -386,6 +428,11 @@ export class DocumentDetailComponent
|
|||||||
error: (error) => {
|
error: (error) => {
|
||||||
this.networkActive = false
|
this.networkActive = false
|
||||||
this.error = error.error
|
this.error = error.error
|
||||||
|
this.toastService.showError(
|
||||||
|
$localize`Error saving document` +
|
||||||
|
': ' +
|
||||||
|
(error.message ?? error.toString())
|
||||||
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -425,6 +472,11 @@ export class DocumentDetailComponent
|
|||||||
error: (error) => {
|
error: (error) => {
|
||||||
this.networkActive = false
|
this.networkActive = false
|
||||||
this.error = error.error
|
this.error = error.error
|
||||||
|
this.toastService.showError(
|
||||||
|
$localize`Error saving document` +
|
||||||
|
': ' +
|
||||||
|
(error.message ?? error.toString())
|
||||||
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -564,6 +616,36 @@ export class DocumentDetailComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
get commentsEnabled(): boolean {
|
get commentsEnabled(): boolean {
|
||||||
return this.settings.get(SETTINGS_KEYS.COMMENTS_ENABLED)
|
return (
|
||||||
|
this.settings.get(SETTINGS_KEYS.COMMENTS_ENABLED) &&
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.View,
|
||||||
|
PermissionType.Document
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
get userIsOwner(): boolean {
|
||||||
|
let doc: PaperlessDocument = Object.assign({}, this.document)
|
||||||
|
// dont disable while editing
|
||||||
|
if (this.document && this.store?.value.owner) {
|
||||||
|
doc.owner = this.store?.value.owner
|
||||||
|
}
|
||||||
|
return !this.document || this.permissionsService.currentUserOwnsObject(doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
get userCanEdit(): boolean {
|
||||||
|
let doc: PaperlessDocument = Object.assign({}, this.document)
|
||||||
|
// dont disable while editing
|
||||||
|
if (this.document && this.store?.value.owner) {
|
||||||
|
doc.owner = this.store?.value.owner
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
!this.document ||
|
||||||
|
this.permissionsService.currentUserHasObjectPermissions(
|
||||||
|
PermissionAction.Change,
|
||||||
|
doc
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,11 +23,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="w-100 d-xl-none"></div>
|
<div class="w-100 d-xl-none"></div>
|
||||||
<div class="col-auto mb-2 mb-xl-0">
|
<div class="col-auto mb-2 mb-xl-0">
|
||||||
<div class="d-flex">
|
<div class="d-flex" *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
|
||||||
<label class="ms-auto mt-1 mb-0 me-2" i18n>Edit:</label>
|
<label class="ms-auto mt-1 mb-0 me-2" i18n>Edit:</label>
|
||||||
<app-filterable-dropdown class="me-2 me-md-3" title="Tags" icon="tag-fill" i18n-title
|
<app-filterable-dropdown class="me-2 me-md-3" title="Tags" icon="tag-fill" i18n-title
|
||||||
filterPlaceholder="Filter tags" i18n-filterPlaceholder
|
filterPlaceholder="Filter tags" i18n-filterPlaceholder
|
||||||
[items]="tags"
|
[items]="tags"
|
||||||
|
[disabled]="!userCanEditAll"
|
||||||
[editing]="true"
|
[editing]="true"
|
||||||
[multiple]="true"
|
[multiple]="true"
|
||||||
[applyOnClose]="applyOnClose"
|
[applyOnClose]="applyOnClose"
|
||||||
@ -38,6 +39,7 @@
|
|||||||
<app-filterable-dropdown class="me-2 me-md-3" title="Correspondent" icon="person-fill" i18n-title
|
<app-filterable-dropdown class="me-2 me-md-3" title="Correspondent" icon="person-fill" i18n-title
|
||||||
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
|
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
|
||||||
[items]="correspondents"
|
[items]="correspondents"
|
||||||
|
[disabled]="!userCanEditAll"
|
||||||
[editing]="true"
|
[editing]="true"
|
||||||
[applyOnClose]="applyOnClose"
|
[applyOnClose]="applyOnClose"
|
||||||
(opened)="openCorrespondentDropdown()"
|
(opened)="openCorrespondentDropdown()"
|
||||||
@ -47,6 +49,7 @@
|
|||||||
<app-filterable-dropdown class="me-2 me-md-3" title="Document type" icon="file-earmark-fill" i18n-title
|
<app-filterable-dropdown class="me-2 me-md-3" title="Document type" icon="file-earmark-fill" i18n-title
|
||||||
filterPlaceholder="Filter document types" i18n-filterPlaceholder
|
filterPlaceholder="Filter document types" i18n-filterPlaceholder
|
||||||
[items]="documentTypes"
|
[items]="documentTypes"
|
||||||
|
[disabled]="!userCanEditAll"
|
||||||
[editing]="true"
|
[editing]="true"
|
||||||
[applyOnClose]="applyOnClose"
|
[applyOnClose]="applyOnClose"
|
||||||
(opened)="openDocumentTypeDropdown()"
|
(opened)="openDocumentTypeDropdown()"
|
||||||
@ -56,6 +59,7 @@
|
|||||||
<app-filterable-dropdown class="me-2 me-md-3" title="Storage path" icon="folder-fill" i18n-title
|
<app-filterable-dropdown class="me-2 me-md-3" title="Storage path" icon="folder-fill" i18n-title
|
||||||
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
|
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
|
||||||
[items]="storagePaths"
|
[items]="storagePaths"
|
||||||
|
[disabled]="!userCanEditAll"
|
||||||
[editing]="true"
|
[editing]="true"
|
||||||
[applyOnClose]="applyOnClose"
|
[applyOnClose]="applyOnClose"
|
||||||
(opened)="openStoragePathDropdown()"
|
(opened)="openStoragePathDropdown()"
|
||||||
@ -65,7 +69,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto ms-auto mb-2 mb-xl-0 d-flex">
|
<div class="col-auto ms-auto mb-2 mb-xl-0 d-flex">
|
||||||
<div class="btn-group btn-group-sm me-2">
|
<div class="btn-toolbar me-2">
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="setPermissions()" [disabled]="!userOwnsAll">
|
||||||
|
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" />
|
||||||
|
</svg> <ng-container i18n>Permissions</ng-container>
|
||||||
|
</button>
|
||||||
|
|
||||||
<div ngbDropdown class="me-2 d-flex">
|
<div ngbDropdown class="me-2 d-flex">
|
||||||
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
|
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
|
||||||
<svg class="toolbaricon" fill="currentColor">
|
<svg class="toolbaricon" fill="currentColor">
|
||||||
@ -74,7 +85,7 @@
|
|||||||
<div class="d-none d-sm-inline"> <ng-container i18n>Actions</ng-container></div>
|
<div class="d-none d-sm-inline"> <ng-container i18n>Actions</ng-container></div>
|
||||||
</button>
|
</button>
|
||||||
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
|
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
|
||||||
<button ngbDropdownItem (click)="redoOcrSelected()" i18n>Redo OCR</button>
|
<button ngbDropdownItem (click)="redoOcrSelected()" [disabled]="!userCanEditAll" i18n>Redo OCR</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -120,7 +131,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-group btn-group-sm me-2">
|
<div class="btn-group btn-group-sm me-2">
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()">
|
<button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()" *appIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Document }" [disabled]="!userOwnsAll">
|
||||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||||
</svg> <ng-container i18n>Delete</ng-container>
|
</svg> <ng-container i18n>Delete</ng-container>
|
||||||
|
@ -25,6 +25,9 @@ import { saveAs } from 'file-saver'
|
|||||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||||
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||||
|
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||||
|
import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
|
||||||
|
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||||
import { FormControl, FormGroup } from '@angular/forms'
|
import { FormControl, FormGroup } from '@angular/forms'
|
||||||
import { first, Subject, takeUntil } from 'rxjs'
|
import { first, Subject, takeUntil } from 'rxjs'
|
||||||
|
|
||||||
@ -33,7 +36,10 @@ import { first, Subject, takeUntil } from 'rxjs'
|
|||||||
templateUrl: './bulk-editor.component.html',
|
templateUrl: './bulk-editor.component.html',
|
||||||
styleUrls: ['./bulk-editor.component.scss'],
|
styleUrls: ['./bulk-editor.component.scss'],
|
||||||
})
|
})
|
||||||
export class BulkEditorComponent implements OnInit, OnDestroy {
|
export class BulkEditorComponent
|
||||||
|
extends ComponentWithPermissions
|
||||||
|
implements OnInit, OnDestroy
|
||||||
|
{
|
||||||
tags: PaperlessTag[]
|
tags: PaperlessTag[]
|
||||||
correspondents: PaperlessCorrespondent[]
|
correspondents: PaperlessCorrespondent[]
|
||||||
documentTypes: PaperlessDocumentType[]
|
documentTypes: PaperlessDocumentType[]
|
||||||
@ -63,8 +69,11 @@ export class BulkEditorComponent implements OnInit, OnDestroy {
|
|||||||
private openDocumentService: OpenDocumentsService,
|
private openDocumentService: OpenDocumentsService,
|
||||||
private settings: SettingsService,
|
private settings: SettingsService,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private storagePathService: StoragePathService
|
private storagePathService: StoragePathService,
|
||||||
) {}
|
private permissionService: PermissionsService
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
applyOnClose: boolean = this.settings.get(
|
applyOnClose: boolean = this.settings.get(
|
||||||
SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE
|
SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE
|
||||||
@ -73,6 +82,25 @@ export class BulkEditorComponent implements OnInit, OnDestroy {
|
|||||||
SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS
|
SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS
|
||||||
)
|
)
|
||||||
|
|
||||||
|
get userCanEditAll(): boolean {
|
||||||
|
let canEdit: boolean = true
|
||||||
|
const docs = this.list.documents.filter((d) => this.list.selected.has(d.id))
|
||||||
|
canEdit = docs.every((d) =>
|
||||||
|
this.permissionService.currentUserHasObjectPermissions(
|
||||||
|
this.PermissionAction.Change,
|
||||||
|
d
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return canEdit
|
||||||
|
}
|
||||||
|
|
||||||
|
get userOwnsAll(): boolean {
|
||||||
|
let ownsAll: boolean = true
|
||||||
|
const docs = this.list.documents.filter((d) => this.list.selected.has(d.id))
|
||||||
|
ownsAll = docs.every((d) => this.permissionService.currentUserOwnsObject(d))
|
||||||
|
return ownsAll
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.tagService
|
this.tagService
|
||||||
.listAll()
|
.listAll()
|
||||||
@ -463,4 +491,14 @@ export class BulkEditorComponent implements OnInit, OnDestroy {
|
|||||||
this.executeBulkOperation(modal, 'redo_ocr', {})
|
this.executeBulkOperation(modal, 'redo_ocr', {})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPermissions() {
|
||||||
|
let modal = this.modalService.open(PermissionsDialogComponent, {
|
||||||
|
backdrop: 'static',
|
||||||
|
})
|
||||||
|
modal.componentInstance.confirmClicked.subscribe((permissions) => {
|
||||||
|
modal.componentInstance.buttonsEnabled = false
|
||||||
|
this.executeBulkOperation(modal, 'set_permissions', permissions)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@
|
|||||||
<use xlink:href="assets/bootstrap-icons.svg#diagram-3"/>
|
<use xlink:href="assets/bootstrap-icons.svg#diagram-3"/>
|
||||||
</svg> <span class="d-none d-md-inline" i18n>More like this</span>
|
</svg> <span class="d-none d-md-inline" i18n>More like this</span>
|
||||||
</a>
|
</a>
|
||||||
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary">
|
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
|
||||||
<svg class="sidebaricon" fill="currentColor" class="sidebaricon">
|
<svg class="sidebaricon" fill="currentColor" class="sidebaricon">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#pencil"/>
|
<use xlink:href="assets/bootstrap-icons.svg#pencil"/>
|
||||||
</svg> <span class="d-none d-md-inline" i18n>Edit</span>
|
</svg> <span class="d-none d-md-inline" i18n>Edit</span>
|
||||||
|
@ -10,6 +10,7 @@ import { DocumentService } from 'src/app/services/rest/document.service'
|
|||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||||
|
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-document-card-large',
|
selector: 'app-document-card-large',
|
||||||
@ -19,11 +20,13 @@ import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
|||||||
'../popover-preview/popover-preview.scss',
|
'../popover-preview/popover-preview.scss',
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class DocumentCardLargeComponent {
|
export class DocumentCardLargeComponent extends ComponentWithPermissions {
|
||||||
constructor(
|
constructor(
|
||||||
private documentService: DocumentService,
|
private documentService: DocumentService,
|
||||||
private settingsService: SettingsService
|
private settingsService: SettingsService
|
||||||
) {}
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
selected = false
|
selected = false
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<div class="btn-group w-100">
|
<div class="btn-group w-100">
|
||||||
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" title="Edit" i18n-title>
|
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" title="Edit" i18n-title *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }" i18n-title>
|
||||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -11,6 +11,7 @@ import { DocumentService } from 'src/app/services/rest/document.service'
|
|||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||||
|
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-document-card-small',
|
selector: 'app-document-card-small',
|
||||||
@ -20,11 +21,13 @@ import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
|||||||
'../popover-preview/popover-preview.scss',
|
'../popover-preview/popover-preview.scss',
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class DocumentCardSmallComponent {
|
export class DocumentCardSmallComponent extends ComponentWithPermissions {
|
||||||
constructor(
|
constructor(
|
||||||
private documentService: DocumentService,
|
private documentService: DocumentService,
|
||||||
private settingsService: SettingsService
|
private settingsService: SettingsService
|
||||||
) {}
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
selected = false
|
selected = false
|
||||||
|
@ -59,7 +59,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-group ms-2 flex-fill" ngbDropdown role="group">
|
<div class="btn-group ms-2 flex-fill" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }" ngbDropdown role="group">
|
||||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle flex-fill" tourAnchor="tour.documents-views" ngbDropdownToggle>
|
<button class="btn btn-sm btn-outline-primary dropdown-toggle flex-fill" tourAnchor="tour.documents-views" ngbDropdownToggle>
|
||||||
<ng-container i18n>Views</ng-container>
|
<ng-container i18n>Views</ng-container>
|
||||||
<div *ngIf="savedViewIsModified" class="position-absolute top-0 start-100 p-2 translate-middle badge bg-secondary border border-light rounded-circle">
|
<div *ngIf="savedViewIsModified" class="position-absolute top-0 start-100 p-2 translate-middle badge bg-secondary border border-light rounded-circle">
|
||||||
@ -72,8 +72,10 @@
|
|||||||
<div class="dropdown-divider" *ngIf="savedViewService.allViews.length > 0"></div>
|
<div class="dropdown-divider" *ngIf="savedViewService.allViews.length > 0"></div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.activeSavedViewId" [disabled]="!savedViewIsModified" i18n>Save "{{list.activeSavedViewTitle}}"</button>
|
<div *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.SavedView }">
|
||||||
<button ngbDropdownItem (click)="saveViewConfigAs()" i18n>Save as...</button>
|
<button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.activeSavedViewId" [disabled]="!savedViewIsModified" i18n>Save "{{list.activeSavedViewTitle}}"</button>
|
||||||
|
</div>
|
||||||
|
<button ngbDropdownItem (click)="saveViewConfigAs()" *appIfPermissions="{ action: PermissionAction.Add, type: PermissionType.SavedView }" i18n>Save as...</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -30,6 +30,7 @@ import {
|
|||||||
} from 'src/app/services/rest/document.service'
|
} from 'src/app/services/rest/document.service'
|
||||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
||||||
import { FilterEditorComponent } from './filter-editor/filter-editor.component'
|
import { FilterEditorComponent } from './filter-editor/filter-editor.component'
|
||||||
import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component'
|
import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component'
|
||||||
|
|
||||||
@ -38,7 +39,10 @@ import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-vi
|
|||||||
templateUrl: './document-list.component.html',
|
templateUrl: './document-list.component.html',
|
||||||
styleUrls: ['./document-list.component.scss'],
|
styleUrls: ['./document-list.component.scss'],
|
||||||
})
|
})
|
||||||
export class DocumentListComponent implements OnInit, OnDestroy {
|
export class DocumentListComponent
|
||||||
|
extends ComponentWithPermissions
|
||||||
|
implements OnInit, OnDestroy
|
||||||
|
{
|
||||||
constructor(
|
constructor(
|
||||||
public list: DocumentListViewService,
|
public list: DocumentListViewService,
|
||||||
public savedViewService: SavedViewService,
|
public savedViewService: SavedViewService,
|
||||||
@ -48,7 +52,9 @@ export class DocumentListComponent implements OnInit, OnDestroy {
|
|||||||
private modalService: NgbModal,
|
private modalService: NgbModal,
|
||||||
private consumerStatusService: ConsumerStatusService,
|
private consumerStatusService: ConsumerStatusService,
|
||||||
public openDocumentsService: OpenDocumentsService
|
public openDocumentsService: OpenDocumentsService
|
||||||
) {}
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
@ViewChild('filterEditor')
|
@ViewChild('filterEditor')
|
||||||
private filterEditor: FilterEditorComponent
|
private filterEditor: FilterEditorComponent
|
||||||
|
@ -4,6 +4,10 @@ import { FILTER_CORRESPONDENT } from 'src/app/data/filter-rule-type'
|
|||||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
||||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||||
|
import {
|
||||||
|
PermissionsService,
|
||||||
|
PermissionType,
|
||||||
|
} from 'src/app/services/permissions.service'
|
||||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
||||||
@ -21,6 +25,7 @@ export class CorrespondentListComponent extends ManagementListComponent<Paperles
|
|||||||
modalService: NgbModal,
|
modalService: NgbModal,
|
||||||
toastService: ToastService,
|
toastService: ToastService,
|
||||||
documentListViewService: DocumentListViewService,
|
documentListViewService: DocumentListViewService,
|
||||||
|
permissionsService: PermissionsService,
|
||||||
private datePipe: CustomDatePipe
|
private datePipe: CustomDatePipe
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
@ -29,9 +34,11 @@ export class CorrespondentListComponent extends ManagementListComponent<Paperles
|
|||||||
CorrespondentEditDialogComponent,
|
CorrespondentEditDialogComponent,
|
||||||
toastService,
|
toastService,
|
||||||
documentListViewService,
|
documentListViewService,
|
||||||
|
permissionsService,
|
||||||
FILTER_CORRESPONDENT,
|
FILTER_CORRESPONDENT,
|
||||||
$localize`correspondent`,
|
$localize`correspondent`,
|
||||||
$localize`correspondents`,
|
$localize`correspondents`,
|
||||||
|
PermissionType.Correspondent,
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
key: 'last_correspondence',
|
key: 'last_correspondence',
|
||||||
|
@ -3,6 +3,10 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
|||||||
import { FILTER_DOCUMENT_TYPE } from 'src/app/data/filter-rule-type'
|
import { FILTER_DOCUMENT_TYPE } from 'src/app/data/filter-rule-type'
|
||||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
|
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
|
||||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||||
|
import {
|
||||||
|
PermissionsService,
|
||||||
|
PermissionType,
|
||||||
|
} from 'src/app/services/permissions.service'
|
||||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
||||||
@ -18,7 +22,8 @@ export class DocumentTypeListComponent extends ManagementListComponent<Paperless
|
|||||||
documentTypeService: DocumentTypeService,
|
documentTypeService: DocumentTypeService,
|
||||||
modalService: NgbModal,
|
modalService: NgbModal,
|
||||||
toastService: ToastService,
|
toastService: ToastService,
|
||||||
documentListViewService: DocumentListViewService
|
documentListViewService: DocumentListViewService,
|
||||||
|
permissionsService: PermissionsService
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
documentTypeService,
|
documentTypeService,
|
||||||
@ -26,9 +31,11 @@ export class DocumentTypeListComponent extends ManagementListComponent<Paperless
|
|||||||
DocumentTypeEditDialogComponent,
|
DocumentTypeEditDialogComponent,
|
||||||
toastService,
|
toastService,
|
||||||
documentListViewService,
|
documentListViewService,
|
||||||
|
permissionsService,
|
||||||
FILTER_DOCUMENT_TYPE,
|
FILTER_DOCUMENT_TYPE,
|
||||||
$localize`document type`,
|
$localize`document type`,
|
||||||
$localize`document types`,
|
$localize`document types`,
|
||||||
|
PermissionType.DocumentType,
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<app-page-header title="{{ typeNamePlural | titlecase }}">
|
<app-page-header title="{{ typeNamePlural | titlecase }}">
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" i18n>Create</button>
|
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" *appIfPermissions="{ action: PermissionAction.Add, type: permissionType }" i18n>Create</button>
|
||||||
</app-page-header>
|
</app-page-header>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -41,24 +41,24 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
|
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
|
||||||
<button (click)="filterDocuments(object)" ngbDropdownItem i18n>Filter Documents</button>
|
<button (click)="filterDocuments(object)" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }" ngbDropdownItem i18n>Filter Documents</button>
|
||||||
<button (click)="openEditDialog(object)" ngbDropdownItem i18n>Edit</button>
|
<button (click)="openEditDialog(object)" *appIfPermissions="{ action: PermissionAction.Change, type: permissionType }" ngbDropdownItem i18n>Edit</button>
|
||||||
<button class="text-danger" (click)="openDeleteDialog(object)" ngbDropdownItem i18n>Delete</button>
|
<button class="text-danger" (click)="openDeleteDialog(object)" *appIfPermissions="{ action: PermissionAction.Delete, type: permissionType }" ngbDropdownItem i18n>Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group d-none d-sm-block">
|
<div class="btn-group d-none d-sm-block">
|
||||||
<button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(object)">
|
<button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(object)" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-funnel" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-funnel" viewBox="0 0 16 16">
|
||||||
<path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2h-11z"/>
|
<path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2h-11z"/>
|
||||||
</svg> <ng-container i18n>Documents</ng-container>
|
</svg> <ng-container i18n>Documents</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(object)">
|
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(object)" *appIfPermissions="{ action: PermissionAction.Change, type: permissionType }" [disabled]="!userCanEdit(object)">
|
||||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||||||
</svg> <ng-container i18n>Edit</ng-container>
|
</svg> <ng-container i18n>Edit</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(object)">
|
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(object)" *appIfPermissions="{ action: PermissionAction.Delete, type: permissionType }" [disabled]="!userCanDelete(object)">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
||||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
||||||
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
||||||
|
@ -14,14 +14,20 @@ import {
|
|||||||
MATCH_AUTO,
|
MATCH_AUTO,
|
||||||
} from 'src/app/data/matching-model'
|
} from 'src/app/data/matching-model'
|
||||||
import { ObjectWithId } from 'src/app/data/object-with-id'
|
import { ObjectWithId } from 'src/app/data/object-with-id'
|
||||||
|
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
|
||||||
import {
|
import {
|
||||||
SortableDirective,
|
SortableDirective,
|
||||||
SortEvent,
|
SortEvent,
|
||||||
} from 'src/app/directives/sortable.directive'
|
} from 'src/app/directives/sortable.directive'
|
||||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||||
|
import {
|
||||||
|
PermissionsService,
|
||||||
|
PermissionType,
|
||||||
|
} from 'src/app/services/permissions.service'
|
||||||
import { AbstractNameFilterService } from 'src/app/services/rest/abstract-name-filter-service'
|
import { AbstractNameFilterService } from 'src/app/services/rest/abstract-name-filter-service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||||
|
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||||
|
|
||||||
export interface ManagementListColumn {
|
export interface ManagementListColumn {
|
||||||
key: string
|
key: string
|
||||||
@ -35,6 +41,7 @@ export interface ManagementListColumn {
|
|||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export abstract class ManagementListComponent<T extends ObjectWithId>
|
export abstract class ManagementListComponent<T extends ObjectWithId>
|
||||||
|
extends ComponentWithPermissions
|
||||||
implements OnInit, OnDestroy
|
implements OnInit, OnDestroy
|
||||||
{
|
{
|
||||||
constructor(
|
constructor(
|
||||||
@ -43,11 +50,15 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
|
|||||||
private editDialogComponent: any,
|
private editDialogComponent: any,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private documentListViewService: DocumentListViewService,
|
private documentListViewService: DocumentListViewService,
|
||||||
|
private permissionsService: PermissionsService,
|
||||||
protected filterRuleType: number,
|
protected filterRuleType: number,
|
||||||
public typeName: string,
|
public typeName: string,
|
||||||
public typeNamePlural: string,
|
public typeNamePlural: string,
|
||||||
|
public permissionType: PermissionType,
|
||||||
public extraColumns: ManagementListColumn[]
|
public extraColumns: ManagementListColumn[]
|
||||||
) {}
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
@ViewChildren(SortableDirective) headers: QueryList<SortableDirective>
|
@ViewChildren(SortableDirective) headers: QueryList<SortableDirective>
|
||||||
|
|
||||||
@ -209,4 +220,15 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
|
|||||||
onNameFilterKeyUp(event: KeyboardEvent) {
|
onNameFilterKeyUp(event: KeyboardEvent) {
|
||||||
if (event.code == 'Escape') this.nameFilterDebounce.next(null)
|
if (event.code == 'Escape') this.nameFilterDebounce.next(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userCanDelete(object: ObjectWithPermissions): boolean {
|
||||||
|
return this.permissionsService.currentUserOwnsObject(object)
|
||||||
|
}
|
||||||
|
|
||||||
|
userCanEdit(object: ObjectWithPermissions): boolean {
|
||||||
|
return this.permissionsService.currentUserHasObjectPermissions(
|
||||||
|
this.PermissionAction.Change,
|
||||||
|
object
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<app-page-header title="Settings" i18n-title>
|
<app-page-header title="Settings" i18n-title>
|
||||||
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()"><ng-container i18n>Start tour</ng-container></button>
|
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()"><ng-container i18n>Start tour</ng-container></button>
|
||||||
<a class="btn btn-sm btn-primary ms-3" href="admin/" target="_blank">
|
<a *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }" class="btn btn-sm btn-primary ms-3" href="admin/" target="_blank">
|
||||||
<ng-container i18n>Open Django Admin</ng-container>
|
<ng-container i18n>Open Django Admin</ng-container>
|
||||||
<svg class="sidebaricon ms-1" fill="currentColor">
|
<svg class="sidebaricon ms-1" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#arrow-up-right"/>
|
<use xlink:href="assets/bootstrap-icons.svg#arrow-up-right"/>
|
||||||
@ -189,6 +189,14 @@
|
|||||||
<a ngbNavLink i18n>Saved views</a>
|
<a ngbNavLink i18n>Saved views</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
|
|
||||||
|
<h4 i18n>Settings</h4>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="offset-md-3 col">
|
||||||
|
<app-input-check i18n-title title="Show warning when closing saved views with unsaved changes" formControlName="savedViewsWarnOnUnsavedChange"></app-input-check>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 i18n>Views</h4>
|
||||||
<div formGroupName="savedViews">
|
<div formGroupName="savedViews">
|
||||||
|
|
||||||
<div *ngFor="let view of savedViews" [formGroupName]="view.id" class="row">
|
<div *ngFor="let view of savedViews" [formGroupName]="view.id" class="row">
|
||||||
@ -211,7 +219,7 @@
|
|||||||
|
|
||||||
<div class="mb-2 col-auto">
|
<div class="mb-2 col-auto">
|
||||||
<label class="form-label" for="name_{{view.id}}" i18n>Actions</label>
|
<label class="form-label" for="name_{{view.id}}" i18n>Actions</label>
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger form-control" (click)="deleteSavedView(view)" i18n>Delete</button>
|
<button type="button" class="btn btn-sm btn-outline-danger form-control" (click)="deleteSavedView(view)" *appIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.SavedView }" i18n>Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -227,80 +235,84 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li [ngbNavItem]="SettingsNavIDs.Mail" (mouseover)="maybeInitializeTab(SettingsNavIDs.Mail)" (focusin)="maybeInitializeTab(SettingsNavIDs.Mail)">
|
<li *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailRule }" [ngbNavItem]="SettingsNavIDs.Mail" (mouseover)="maybeInitializeTab(SettingsNavIDs.Mail)" (focusin)="maybeInitializeTab(SettingsNavIDs.Mail)">
|
||||||
<a ngbNavLink i18n>Mail</a>
|
<a ngbNavLink i18n>Mail</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
|
|
||||||
<ng-container *ngIf="mailAccounts && mailRules">
|
<ng-container *ngIf="mailAccounts && mailRules">
|
||||||
<h4>
|
<ng-container *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailAccount }">
|
||||||
<ng-container i18n>Mail accounts</ng-container>
|
<h4>
|
||||||
<button type="button" class="btn btn-sm btn-primary ms-4" (click)="editMailAccount()">
|
<ng-container i18n>Mail accounts</ng-container>
|
||||||
<svg class="sidebaricon me-1" fill="currentColor">
|
<button type="button" class="btn btn-sm btn-primary ms-4" (click)="editMailAccount()">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
<svg class="sidebaricon me-1" fill="currentColor">
|
||||||
</svg>
|
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||||
<ng-container i18n>Add Account</ng-container>
|
</svg>
|
||||||
</button>
|
<ng-container i18n>Add Account</ng-container>
|
||||||
</h4>
|
</button>
|
||||||
<ul class="list-group" formGroupName="mailAccounts">
|
</h4>
|
||||||
|
<ul class="list-group" formGroupName="mailAccounts">
|
||||||
|
|
||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col" i18n>Name</div>
|
<div class="col" i18n>Name</div>
|
||||||
<div class="col" i18n>Server</div>
|
<div class="col" i18n>Server</div>
|
||||||
<div class="col" i18n>Actions</div>
|
<div class="col" i18n>Actions</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li *ngFor="let account of mailAccounts" class="list-group-item" [formGroupName]="account.id">
|
<li *ngFor="let account of mailAccounts" class="list-group-item" [formGroupName]="account.id">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailAccount(account)">{{account.name}}</button></div>
|
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailAccount(account)">{{account.name}}</button></div>
|
||||||
<div class="col d-flex align-items-center">{{account.imap_server}}</div>
|
<div class="col d-flex align-items-center">{{account.imap_server}}</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-sm btn-primary" type="button" (click)="editMailAccount(account)" i18n>Edit</button>
|
<button *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailAccount }" class="btn btn-sm btn-primary" type="button" (click)="editMailAccount(account)" i18n>Edit</button>
|
||||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteMailAccount(account)" i18n>Delete</button>
|
<button *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailAccount }" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteMailAccount(account)" i18n>Delete</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
|
||||||
|
|
||||||
<div *ngIf="mailAccounts.length === 0" i18n>No mail accounts defined.</div>
|
<div *ngIf="mailAccounts.length === 0" i18n>No mail accounts defined.</div>
|
||||||
</ul>
|
</ul>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<h4 class="mt-4">
|
<ng-container *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailRule }">
|
||||||
<ng-container i18n>Mail rules</ng-container>
|
<h4 class="mt-4">
|
||||||
<button type="button" class="btn btn-sm btn-primary ms-4" (click)="editMailRule()">
|
<ng-container i18n>Mail rules</ng-container>
|
||||||
<svg class="sidebaricon me-1" fill="currentColor">
|
<button type="button" class="btn btn-sm btn-primary ms-4" (click)="editMailRule()">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
<svg class="sidebaricon me-1" fill="currentColor">
|
||||||
</svg>
|
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||||
<ng-container i18n>Add Rule</ng-container>
|
</svg>
|
||||||
</button>
|
<ng-container i18n>Add Rule</ng-container>
|
||||||
</h4>
|
</button>
|
||||||
<ul class="list-group" formGroupName="mailRules">
|
</h4>
|
||||||
|
<ul class="list-group" formGroupName="mailRules">
|
||||||
|
|
||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col" i18n>Name</div>
|
<div class="col" i18n>Name</div>
|
||||||
<div class="col" i18n>Account</div>
|
<div class="col" i18n>Account</div>
|
||||||
<div class="col" i18n>Actions</div>
|
<div class="col" i18n>Actions</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li *ngFor="let rule of mailRules" class="list-group-item" [formGroupName]="rule.id">
|
<li *ngFor="let rule of mailRules" class="list-group-item" [formGroupName]="rule.id">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailRule(rule)">{{rule.name}}</button></div>
|
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailRule(rule)">{{rule.name}}</button></div>
|
||||||
<div class="col d-flex align-items-center">{{(mailAccountService.getCached(rule.account) | async)?.name}}</div>
|
<div class="col d-flex align-items-center">{{(mailAccountService.getCached(rule.account) | async)?.name}}</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-sm btn-primary" type="button" (click)="editMailRule(rule)" i18n>Edit</button>
|
<button *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailRule }" class="btn btn-sm btn-primary" type="button" (click)="editMailRule(rule)" i18n>Edit</button>
|
||||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteMailRule(rule)" i18n>Delete</button>
|
<button *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailRule }" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteMailRule(rule)" i18n>Delete</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
|
||||||
|
|
||||||
<div *ngIf="mailRules.length === 0" i18n>No mail rules defined.</div>
|
<div *ngIf="mailRules.length === 0" i18n>No mail rules defined.</div>
|
||||||
</ul>
|
</ul>
|
||||||
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<div *ngIf="!mailAccounts || !mailRules">
|
<div *ngIf="!mailAccounts || !mailRules">
|
||||||
@ -310,9 +322,95 @@
|
|||||||
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li [ngbNavItem]="SettingsNavIDs.UsersGroups" *appIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }" (mouseover)="maybeInitializeTab(SettingsNavIDs.UsersGroups)" (focusin)="maybeInitializeTab(SettingsNavIDs.UsersGroups)">
|
||||||
|
<a ngbNavLink i18n>Users & Groups</a>
|
||||||
|
<ng-template ngbNavContent>
|
||||||
|
|
||||||
|
<ng-container *ngIf="users && groups">
|
||||||
|
<h4 class="d-flex">
|
||||||
|
<ng-container i18n>Users</ng-container>
|
||||||
|
<button type="button" class="btn btn-sm btn-primary ms-4" (click)="editUser()">
|
||||||
|
<svg class="sidebaricon me-1" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||||
|
</svg>
|
||||||
|
<ng-container i18n>Add User</ng-container>
|
||||||
|
</button>
|
||||||
|
</h4>
|
||||||
|
<ul class="list-group" formGroupName="usersGroup">
|
||||||
|
|
||||||
|
<li class="list-group-item">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col" i18n>Username</div>
|
||||||
|
<div class="col" i18n>Name</div>
|
||||||
|
<div class="col" i18n>Groups</div>
|
||||||
|
<div class="col" i18n>Actions</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li *ngFor="let user of users" class="list-group-item" [formGroupName]="user.id">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editUser(user)">{{user.username}}</button></div>
|
||||||
|
<div class="col d-flex align-items-center">{{user.first_name}} {{user.last_name}}</div>
|
||||||
|
<div class="col d-flex align-items-center">{{user.groups?.map(getGroupName, this).join(', ')}}</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-sm btn-primary" type="button" (click)="editUser(user)" i18n>Edit</button>
|
||||||
|
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteUser(user)" i18n>Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4 class="mt-4 d-flex">
|
||||||
|
<ng-container i18n>Groups</ng-container>
|
||||||
|
<button type="button" class="btn btn-sm btn-primary ms-4" (click)="editGroup()">
|
||||||
|
<svg class="sidebaricon me-1" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||||
|
</svg>
|
||||||
|
<ng-container i18n>Add Group</ng-container>
|
||||||
|
</button>
|
||||||
|
</h4>
|
||||||
|
<ul *ngIf="groups.length > 0" class="list-group" formGroupName="groupsGroup">
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<li *ngFor="let group of groups" class="list-group-item" [formGroupName]="group.id">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editGroup(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-primary" type="button" (click)="editGroup(group)" i18n>Edit</button>
|
||||||
|
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" i18n>Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div *ngIf="groups.length === 0">No groups defined</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<div *ngIf="!users || !groups">
|
||||||
|
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||||
|
<div class="visually-hidden" i18n>Loading...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</ng-template>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
|
<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="submit" class="btn btn-primary mb-2" *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.UISettings }" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
|
||||||
</form>
|
</form>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user