Merge remote-tracking branch 'paperless/dev' into feature-consume-eml
							
								
								
									
										254
									
								
								.github/scripts/cleanup-tags.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,254 @@ | |||||||
|  | import logging | ||||||
|  | import os | ||||||
|  | from argparse import ArgumentParser | ||||||
|  | from typing import Final | ||||||
|  | from typing import List | ||||||
|  | from urllib.parse import quote | ||||||
|  |  | ||||||
|  | import requests | ||||||
|  | from common import get_log_level | ||||||
|  |  | ||||||
|  | logger = logging.getLogger("cleanup-tags") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class GithubContainerRegistry: | ||||||
|  |     def __init__( | ||||||
|  |         self, | ||||||
|  |         session: requests.Session, | ||||||
|  |         token: str, | ||||||
|  |         owner_or_org: str, | ||||||
|  |     ): | ||||||
|  |         self._session: requests.Session = session | ||||||
|  |         self._token = token | ||||||
|  |         self._owner_or_org = owner_or_org | ||||||
|  |         # https://docs.github.com/en/rest/branches/branches | ||||||
|  |         self._BRANCHES_ENDPOINT = "https://api.github.com/repos/{OWNER}/{REPO}/branches" | ||||||
|  |         if self._owner_or_org == "paperless-ngx": | ||||||
|  |             # https://docs.github.com/en/rest/packages#get-all-package-versions-for-a-package-owned-by-an-organization | ||||||
|  |             self._PACKAGES_VERSIONS_ENDPOINT = "https://api.github.com/orgs/{ORG}/packages/{PACKAGE_TYPE}/{PACKAGE_NAME}/versions" | ||||||
|  |             # https://docs.github.com/en/rest/packages#delete-package-version-for-an-organization | ||||||
|  |             self._PACKAGE_VERSION_DELETE_ENDPOINT = "https://api.github.com/orgs/{ORG}/packages/{PACKAGE_TYPE}/{PACKAGE_NAME}/versions/{PACKAGE_VERSION_ID}" | ||||||
|  |         else: | ||||||
|  |             # https://docs.github.com/en/rest/packages#get-all-package-versions-for-a-package-owned-by-the-authenticated-user | ||||||
|  |             self._PACKAGES_VERSIONS_ENDPOINT = "https://api.github.com/user/packages/{PACKAGE_TYPE}/{PACKAGE_NAME}/versions" | ||||||
|  |             # https://docs.github.com/en/rest/packages#delete-a-package-version-for-the-authenticated-user | ||||||
|  |             self._PACKAGE_VERSION_DELETE_ENDPOINT = "https://api.github.com/user/packages/{PACKAGE_TYPE}/{PACKAGE_NAME}/versions/{PACKAGE_VERSION_ID}" | ||||||
|  |  | ||||||
|  |     def __enter__(self): | ||||||
|  |         self._session.headers.update( | ||||||
|  |             { | ||||||
|  |                 "Accept": "application/vnd.github.v3+json", | ||||||
|  |                 "Authorization": f"token {self._token}", | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |         return self | ||||||
|  |  | ||||||
|  |     def __exit__(self, exc_type, exc_val, exc_tb): | ||||||
|  |         if "Accept" in self._session.headers: | ||||||
|  |             del self._session.headers["Accept"] | ||||||
|  |         if "Authorization" in self._session.headers: | ||||||
|  |             del self._session.headers["Authorization"] | ||||||
|  |  | ||||||
|  |     def _read_all_pages(self, endpoint): | ||||||
|  |         internal_data = [] | ||||||
|  |  | ||||||
|  |         while True: | ||||||
|  |             resp = self._session.get(endpoint) | ||||||
|  |             if resp.status_code == 200: | ||||||
|  |                 internal_data += resp.json() | ||||||
|  |                 if "next" in resp.links: | ||||||
|  |                     endpoint = resp.links["next"]["url"] | ||||||
|  |                 else: | ||||||
|  |                     logger.debug("Exiting pagination loop") | ||||||
|  |                     break | ||||||
|  |             else: | ||||||
|  |                 logger.warning(f"Request to {endpoint} return HTTP {resp.status_code}") | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |         return internal_data | ||||||
|  |  | ||||||
|  |     def get_branches(self, repo: str): | ||||||
|  |         endpoint = self._BRANCHES_ENDPOINT.format(OWNER=self._owner_or_org, REPO=repo) | ||||||
|  |         internal_data = self._read_all_pages(endpoint) | ||||||
|  |         return internal_data | ||||||
|  |  | ||||||
|  |     def filter_branches_by_name_pattern(self, branch_data, pattern: str): | ||||||
|  |         matches = {} | ||||||
|  |  | ||||||
|  |         for branch in branch_data: | ||||||
|  |             if branch["name"].startswith(pattern): | ||||||
|  |                 matches[branch["name"]] = branch | ||||||
|  |  | ||||||
|  |         return matches | ||||||
|  |  | ||||||
|  |     def get_package_versions( | ||||||
|  |         self, | ||||||
|  |         package_name: str, | ||||||
|  |         package_type: str = "container", | ||||||
|  |     ) -> List: | ||||||
|  |         package_name = quote(package_name, safe="") | ||||||
|  |         endpoint = self._PACKAGES_VERSIONS_ENDPOINT.format( | ||||||
|  |             ORG=self._owner_or_org, | ||||||
|  |             PACKAGE_TYPE=package_type, | ||||||
|  |             PACKAGE_NAME=package_name, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         internal_data = self._read_all_pages(endpoint) | ||||||
|  |  | ||||||
|  |         return internal_data | ||||||
|  |  | ||||||
|  |     def filter_packages_by_tag_pattern(self, package_data, pattern: str): | ||||||
|  |         matches = {} | ||||||
|  |  | ||||||
|  |         for package in package_data: | ||||||
|  |             if "metadata" in package and "container" in package["metadata"]: | ||||||
|  |                 container_metadata = package["metadata"]["container"] | ||||||
|  |                 if "tags" in container_metadata: | ||||||
|  |                     container_tags = container_metadata["tags"] | ||||||
|  |                     for tag in container_tags: | ||||||
|  |                         if tag.startswith(pattern): | ||||||
|  |                             matches[tag] = package | ||||||
|  |                             break | ||||||
|  |  | ||||||
|  |         return matches | ||||||
|  |  | ||||||
|  |     def filter_packages_untagged(self, package_data): | ||||||
|  |         matches = {} | ||||||
|  |  | ||||||
|  |         for package in package_data: | ||||||
|  |             if "metadata" in package and "container" in package["metadata"]: | ||||||
|  |                 container_metadata = package["metadata"]["container"] | ||||||
|  |                 if "tags" in container_metadata: | ||||||
|  |                     container_tags = container_metadata["tags"] | ||||||
|  |                     if not len(container_tags): | ||||||
|  |                         matches[package["name"]] = package | ||||||
|  |  | ||||||
|  |         return matches | ||||||
|  |  | ||||||
|  |     def delete_package_version(self, package_name, package_data): | ||||||
|  |         package_name = quote(package_name, safe="") | ||||||
|  |         endpoint = self._PACKAGE_VERSION_DELETE_ENDPOINT.format( | ||||||
|  |             ORG=self._owner_or_org, | ||||||
|  |             PACKAGE_TYPE=package_data["metadata"]["package_type"], | ||||||
|  |             PACKAGE_NAME=package_name, | ||||||
|  |             PACKAGE_VERSION_ID=package_data["id"], | ||||||
|  |         ) | ||||||
|  |         resp = self._session.delete(endpoint) | ||||||
|  |         if resp.status_code != 204: | ||||||
|  |             logger.warning( | ||||||
|  |                 f"Request to delete {endpoint} returned HTTP {resp.status_code}", | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _main(): | ||||||
|  |     parser = ArgumentParser( | ||||||
|  |         description="Using the GitHub API locate and optionally delete container" | ||||||
|  |         " tags which no longer have an associated feature branch", | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     parser.add_argument( | ||||||
|  |         "--delete", | ||||||
|  |         action="store_true", | ||||||
|  |         default=False, | ||||||
|  |         help="If provided, actually delete the container tags", | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # TODO There's a lot of untagged images, do those need to stay for anything? | ||||||
|  |     parser.add_argument( | ||||||
|  |         "--untagged", | ||||||
|  |         action="store_true", | ||||||
|  |         default=False, | ||||||
|  |         help="If provided, delete untagged containers as well", | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     parser.add_argument( | ||||||
|  |         "--loglevel", | ||||||
|  |         default="info", | ||||||
|  |         help="Configures the logging level", | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     args = parser.parse_args() | ||||||
|  |  | ||||||
|  |     logging.basicConfig( | ||||||
|  |         level=get_log_level(args), | ||||||
|  |         datefmt="%Y-%m-%d %H:%M:%S", | ||||||
|  |         format="%(asctime)s %(levelname)-8s %(message)s", | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     repo_owner: Final[str] = os.environ["GITHUB_REPOSITORY_OWNER"] | ||||||
|  |     repo: Final[str] = os.environ["GITHUB_REPOSITORY"] | ||||||
|  |     gh_token: Final[str] = os.environ["GITHUB_TOKEN"] | ||||||
|  |  | ||||||
|  |     with requests.session() as sess: | ||||||
|  |         with GithubContainerRegistry(sess, gh_token, repo_owner) as gh_api: | ||||||
|  |             all_branches = gh_api.get_branches("paperless-ngx") | ||||||
|  |             logger.info(f"Located {len(all_branches)} branches of {repo_owner}/{repo} ") | ||||||
|  |  | ||||||
|  |             feature_branches = gh_api.filter_branches_by_name_pattern( | ||||||
|  |                 all_branches, | ||||||
|  |                 "feature-", | ||||||
|  |             ) | ||||||
|  |             logger.info(f"Located {len(feature_branches)} feature branches") | ||||||
|  |  | ||||||
|  |             for package_name in ["paperless-ngx", "paperless-ngx/builder/cache/app"]: | ||||||
|  |  | ||||||
|  |                 all_package_versions = gh_api.get_package_versions(package_name) | ||||||
|  |                 logger.info( | ||||||
|  |                     f"Located {len(all_package_versions)} versions of package {package_name}", | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|  |                 packages_tagged_feature = gh_api.filter_packages_by_tag_pattern( | ||||||
|  |                     all_package_versions, | ||||||
|  |                     "feature-", | ||||||
|  |                 ) | ||||||
|  |                 logger.info( | ||||||
|  |                     f'Located {len(packages_tagged_feature)} versions of package {package_name} tagged "feature-"', | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|  |                 untagged_packages = gh_api.filter_packages_untagged( | ||||||
|  |                     all_package_versions, | ||||||
|  |                 ) | ||||||
|  |                 logger.info( | ||||||
|  |                     f"Located {len(untagged_packages)} untagged versions of package {package_name}", | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|  |                 to_delete = list( | ||||||
|  |                     set(packages_tagged_feature.keys()) - set(feature_branches.keys()), | ||||||
|  |                 ) | ||||||
|  |                 logger.info( | ||||||
|  |                     f"Located {len(to_delete)} versions of package {package_name} to delete", | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|  |                 for tag_to_delete in to_delete: | ||||||
|  |                     package_version_info = packages_tagged_feature[tag_to_delete] | ||||||
|  |  | ||||||
|  |                     if args.delete: | ||||||
|  |                         logger.info( | ||||||
|  |                             f"Deleting {tag_to_delete} (id {package_version_info['id']})", | ||||||
|  |                         ) | ||||||
|  |                         gh_api.delete_package_version( | ||||||
|  |                             package_name, | ||||||
|  |                             package_version_info, | ||||||
|  |                         ) | ||||||
|  |  | ||||||
|  |                     else: | ||||||
|  |                         logger.info( | ||||||
|  |                             f"Would delete {tag_to_delete} (id {package_version_info['id']})", | ||||||
|  |                         ) | ||||||
|  |  | ||||||
|  |                 if args.untagged: | ||||||
|  |                     logger.info(f"Deleting untagged packages of {package_name}") | ||||||
|  |                     for to_delete_name in untagged_packages: | ||||||
|  |                         to_delete_version = untagged_packages[to_delete_name] | ||||||
|  |                         logger.info(f"Deleting id {to_delete_version['id']}") | ||||||
|  |                         if args.delete: | ||||||
|  |                             gh_api.delete_package_version( | ||||||
|  |                                 package_name, | ||||||
|  |                                 to_delete_version, | ||||||
|  |                             ) | ||||||
|  |                 else: | ||||||
|  |                     logger.info("Leaving untagged images untouched") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     _main() | ||||||
							
								
								
									
										17
									
								
								.github/scripts/common.py
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,4 +1,6 @@ | |||||||
| #!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||||
|  | import logging | ||||||
|  | from argparse import ArgumentError | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_image_tag( | def get_image_tag( | ||||||
| @@ -25,3 +27,18 @@ def get_cache_image_tag( | |||||||
|     rebuilds, generally almost instant for the same version |     rebuilds, generally almost instant for the same version | ||||||
|     """ |     """ | ||||||
|     return f"ghcr.io/{repo_name}/builder/cache/{pkg_name}:{pkg_version}" |     return f"ghcr.io/{repo_name}/builder/cache/{pkg_name}:{pkg_version}" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_log_level(args) -> int: | ||||||
|  |     levels = { | ||||||
|  |         "critical": logging.CRITICAL, | ||||||
|  |         "error": logging.ERROR, | ||||||
|  |         "warn": logging.WARNING, | ||||||
|  |         "warning": logging.WARNING, | ||||||
|  |         "info": logging.INFO, | ||||||
|  |         "debug": logging.DEBUG, | ||||||
|  |     } | ||||||
|  |     level = levels.get(args.loglevel.lower()) | ||||||
|  |     if level is None: | ||||||
|  |         level = logging.INFO | ||||||
|  |     return level | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								.github/scripts/get-build-json.py
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -50,7 +50,6 @@ def _main(): | |||||||
|  |  | ||||||
|     # Default output values |     # Default output values | ||||||
|     version = None |     version = None | ||||||
|     git_tag = None |  | ||||||
|     extra_config = {} |     extra_config = {} | ||||||
|  |  | ||||||
|     if args.package in pipfile_data["default"]: |     if args.package in pipfile_data["default"]: | ||||||
| @@ -59,12 +58,6 @@ def _main(): | |||||||
|         pkg_version = pkg_data["version"].split("==")[-1] |         pkg_version = pkg_data["version"].split("==")[-1] | ||||||
|         version = pkg_version |         version = pkg_version | ||||||
|  |  | ||||||
|         # Based on the package, generate the expected Git tag name |  | ||||||
|         if args.package == "pikepdf": |  | ||||||
|             git_tag = f"v{pkg_version}" |  | ||||||
|         elif args.package == "psycopg2": |  | ||||||
|             git_tag = pkg_version.replace(".", "_") |  | ||||||
|  |  | ||||||
|         # Any extra/special values needed |         # Any extra/special values needed | ||||||
|         if args.package == "pikepdf": |         if args.package == "pikepdf": | ||||||
|             extra_config["qpdf_version"] = build_json["qpdf"]["version"] |             extra_config["qpdf_version"] = build_json["qpdf"]["version"] | ||||||
| @@ -72,8 +65,6 @@ def _main(): | |||||||
|     elif args.package in build_json: |     elif args.package in build_json: | ||||||
|         version = build_json[args.package]["version"] |         version = build_json[args.package]["version"] | ||||||
|  |  | ||||||
|         if "git_tag" in build_json[args.package]: |  | ||||||
|             git_tag = build_json[args.package]["git_tag"] |  | ||||||
|     else: |     else: | ||||||
|         raise NotImplementedError(args.package) |         raise NotImplementedError(args.package) | ||||||
|  |  | ||||||
| @@ -81,7 +72,6 @@ def _main(): | |||||||
|     output = { |     output = { | ||||||
|         "name": args.package, |         "name": args.package, | ||||||
|         "version": version, |         "version": version, | ||||||
|         "git_tag": git_tag, |  | ||||||
|         "image_tag": get_image_tag(repo_name, args.package, version), |         "image_tag": get_image_tag(repo_name, args.package, version), | ||||||
|         "cache_tag": get_cache_image_tag( |         "cache_tag": get_cache_image_tag( | ||||||
|             repo_name, |             repo_name, | ||||||
|   | |||||||
							
								
								
									
										28
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -26,7 +26,7 @@ jobs: | |||||||
|         run: pipx install pipenv |         run: pipx install pipenv | ||||||
|       - |       - | ||||||
|         name: Set up Python |         name: Set up Python | ||||||
|         uses: actions/setup-python@v3 |         uses: actions/setup-python@v4 | ||||||
|         with: |         with: | ||||||
|           python-version: 3.9 |           python-version: 3.9 | ||||||
|           cache: "pipenv" |           cache: "pipenv" | ||||||
| @@ -73,7 +73,7 @@ jobs: | |||||||
|         uses: actions/checkout@v3 |         uses: actions/checkout@v3 | ||||||
|       - |       - | ||||||
|         name: Set up Python |         name: Set up Python | ||||||
|         uses: actions/setup-python@v3 |         uses: actions/setup-python@v4 | ||||||
|         with: |         with: | ||||||
|           python-version: "3.9" |           python-version: "3.9" | ||||||
|       - |       - | ||||||
| @@ -135,18 +135,24 @@ jobs: | |||||||
|       - |       - | ||||||
|         name: Check pushing to Docker Hub |         name: Check pushing to Docker Hub | ||||||
|         id: docker-hub |         id: docker-hub | ||||||
|         # Only push to Dockerhub from the main repo |         # Only push to Dockerhub from the main repo AND the ref is either: | ||||||
|  |         #  main | ||||||
|  |         #  dev | ||||||
|  |         #  beta | ||||||
|  |         #  a tag | ||||||
|         # Otherwise forks would require a Docker Hub account and secrets setup |         # Otherwise forks would require a Docker Hub account and secrets setup | ||||||
|         run: | |         run: | | ||||||
|           if [[ ${{ github.repository }} == "paperless-ngx/paperless-ngx" ]] ; then |           if [[ ${{ github.repository }} == "paperless-ngx/paperless-ngx" && ( ${{ github.ref_name }} == "main" || ${{ github.ref_name }} == "dev" || ${{ github.ref_name }} == "beta" || ${{ startsWith(github.ref, 'refs/tags/v') }} == "true" ) ]] ; then | ||||||
|  |             echo "Enabling DockerHub image push" | ||||||
|             echo ::set-output name=enable::"true" |             echo ::set-output name=enable::"true" | ||||||
|           else |           else | ||||||
|  |             echo "Not pushing to DockerHub" | ||||||
|             echo ::set-output name=enable::"false" |             echo ::set-output name=enable::"false" | ||||||
|           fi |           fi | ||||||
|       - |       - | ||||||
|         name: Gather Docker metadata |         name: Gather Docker metadata | ||||||
|         id: docker-meta |         id: docker-meta | ||||||
|         uses: docker/metadata-action@v3 |         uses: docker/metadata-action@v4 | ||||||
|         with: |         with: | ||||||
|           images: | |           images: | | ||||||
|             ghcr.io/${{ github.repository }} |             ghcr.io/${{ github.repository }} | ||||||
| @@ -163,20 +169,20 @@ jobs: | |||||||
|         uses: actions/checkout@v3 |         uses: actions/checkout@v3 | ||||||
|       - |       - | ||||||
|         name: Set up Docker Buildx |         name: Set up Docker Buildx | ||||||
|         uses: docker/setup-buildx-action@v1 |         uses: docker/setup-buildx-action@v2 | ||||||
|       - |       - | ||||||
|         name: Set up QEMU |         name: Set up QEMU | ||||||
|         uses: docker/setup-qemu-action@v1 |         uses: docker/setup-qemu-action@v2 | ||||||
|       - |       - | ||||||
|         name: Login to Github Container Registry |         name: Login to Github Container Registry | ||||||
|         uses: docker/login-action@v1 |         uses: docker/login-action@v2 | ||||||
|         with: |         with: | ||||||
|           registry: ghcr.io |           registry: ghcr.io | ||||||
|           username: ${{ github.actor }} |           username: ${{ github.actor }} | ||||||
|           password: ${{ secrets.GITHUB_TOKEN }} |           password: ${{ secrets.GITHUB_TOKEN }} | ||||||
|       - |       - | ||||||
|         name: Login to Docker Hub |         name: Login to Docker Hub | ||||||
|         uses: docker/login-action@v1 |         uses: docker/login-action@v2 | ||||||
|         # Don't attempt to login is not pushing to Docker Hub |         # Don't attempt to login is not pushing to Docker Hub | ||||||
|         if: steps.docker-hub.outputs.enable == 'true' |         if: steps.docker-hub.outputs.enable == 'true' | ||||||
|         with: |         with: | ||||||
| @@ -184,7 +190,7 @@ jobs: | |||||||
|           password: ${{ secrets.DOCKERHUB_TOKEN }} |           password: ${{ secrets.DOCKERHUB_TOKEN }} | ||||||
|       - |       - | ||||||
|         name: Build and push |         name: Build and push | ||||||
|         uses: docker/build-push-action@v2 |         uses: docker/build-push-action@v3 | ||||||
|         with: |         with: | ||||||
|           context: . |           context: . | ||||||
|           file: ./Dockerfile |           file: ./Dockerfile | ||||||
| @@ -231,7 +237,7 @@ jobs: | |||||||
|         uses: actions/checkout@v3 |         uses: actions/checkout@v3 | ||||||
|       - |       - | ||||||
|         name: Set up Python |         name: Set up Python | ||||||
|         uses: actions/setup-python@v3 |         uses: actions/setup-python@v4 | ||||||
|         with: |         with: | ||||||
|           python-version: 3.9 |           python-version: 3.9 | ||||||
|       - |       - | ||||||
|   | |||||||
							
								
								
									
										48
									
								
								.github/workflows/cleanup-tags.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,48 @@ | |||||||
|  | name: Cleanup Image Tags | ||||||
|  |  | ||||||
|  | on: | ||||||
|  |   schedule: | ||||||
|  |     - cron: '0 0 * * SAT' | ||||||
|  |   delete: | ||||||
|  |   pull_request: | ||||||
|  |     types: | ||||||
|  |       - closed | ||||||
|  |   push: | ||||||
|  |     paths: | ||||||
|  |       - ".github/workflows/cleanup-tags.yml" | ||||||
|  |       - ".github/scripts/cleanup-tags.py" | ||||||
|  |       - ".github/scripts/common.py" | ||||||
|  |  | ||||||
|  | env: | ||||||
|  |   GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   cleanup: | ||||||
|  |     name: Cleanup Image Tags | ||||||
|  |     runs-on: ubuntu-20.04 | ||||||
|  |     permissions: | ||||||
|  |       packages: write | ||||||
|  |     steps: | ||||||
|  |       - | ||||||
|  |         name: Checkout | ||||||
|  |         uses: actions/checkout@v3 | ||||||
|  |       - | ||||||
|  |         name: Login to Github Container Registry | ||||||
|  |         uses: docker/login-action@v1 | ||||||
|  |         with: | ||||||
|  |           registry: ghcr.io | ||||||
|  |           username: ${{ github.actor }} | ||||||
|  |           password: ${{ secrets.GITHUB_TOKEN }} | ||||||
|  |       - | ||||||
|  |         name: Set up Python | ||||||
|  |         uses: actions/setup-python@v3 | ||||||
|  |         with: | ||||||
|  |           python-version: "3.9" | ||||||
|  |       - | ||||||
|  |         name: Install requests | ||||||
|  |         run: | | ||||||
|  |           python -m pip install requests | ||||||
|  |       - | ||||||
|  |         name: Cleanup feature tags | ||||||
|  |         run: | | ||||||
|  |           python ${GITHUB_WORKSPACE}/.github/scripts/cleanup-tags.py --loglevel info --delete | ||||||
							
								
								
									
										4
									
								
								.github/workflows/installer-library.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -41,7 +41,7 @@ jobs: | |||||||
|         uses: actions/checkout@v3 |         uses: actions/checkout@v3 | ||||||
|       - |       - | ||||||
|         name: Set up Python |         name: Set up Python | ||||||
|         uses: actions/setup-python@v3 |         uses: actions/setup-python@v4 | ||||||
|         with: |         with: | ||||||
|           python-version: "3.9" |           python-version: "3.9" | ||||||
|       - |       - | ||||||
| @@ -122,7 +122,6 @@ jobs: | |||||||
|       dockerfile: ./docker-builders/Dockerfile.psycopg2 |       dockerfile: ./docker-builders/Dockerfile.psycopg2 | ||||||
|       build-json: ${{ needs.prepare-docker-build.outputs.psycopg2-json }} |       build-json: ${{ needs.prepare-docker-build.outputs.psycopg2-json }} | ||||||
|       build-args: | |       build-args: | | ||||||
|         PSYCOPG2_GIT_TAG=${{ fromJSON(needs.prepare-docker-build.outputs.psycopg2-json).git_tag }} |  | ||||||
|         PSYCOPG2_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.psycopg2-json).version }} |         PSYCOPG2_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.psycopg2-json).version }} | ||||||
|  |  | ||||||
|   build-pikepdf-wheel: |   build-pikepdf-wheel: | ||||||
| @@ -137,5 +136,4 @@ jobs: | |||||||
|       build-args: | |       build-args: | | ||||||
|         REPO=${{ github.repository }} |         REPO=${{ github.repository }} | ||||||
|         QPDF_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.qpdf-json).version }} |         QPDF_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.qpdf-json).version }} | ||||||
|         PIKEPDF_GIT_TAG=${{ fromJSON(needs.prepare-docker-build.outputs.pikepdf-json).git_tag }} |  | ||||||
|         PIKEPDF_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.pikepdf-json).version }} |         PIKEPDF_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.pikepdf-json).version }} | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								.github/workflows/reusable-ci-backend.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -65,7 +65,7 @@ jobs: | |||||||
|         run: pipx install pipenv |         run: pipx install pipenv | ||||||
|       - |       - | ||||||
|         name: Set up Python |         name: Set up Python | ||||||
|         uses: actions/setup-python@v3 |         uses: actions/setup-python@v4 | ||||||
|         with: |         with: | ||||||
|           python-version: "${{ matrix.python-version }}" |           python-version: "${{ matrix.python-version }}" | ||||||
|           cache: "pipenv" |           cache: "pipenv" | ||||||
| @@ -74,7 +74,7 @@ jobs: | |||||||
|         name: Install system dependencies |         name: Install system dependencies | ||||||
|         run: | |         run: | | ||||||
|           sudo apt-get update -qq |           sudo apt-get update -qq | ||||||
|           sudo apt-get install -qq --no-install-recommends unpaper tesseract-ocr imagemagick ghostscript optipng libzbar0 poppler-utils |           sudo apt-get install -qq --no-install-recommends unpaper tesseract-ocr imagemagick ghostscript libzbar0 poppler-utils | ||||||
|       - |       - | ||||||
|         name: Install Python dependencies |         name: Install Python dependencies | ||||||
|         run: | |         run: | | ||||||
| @@ -87,7 +87,7 @@ jobs: | |||||||
|       - |       - | ||||||
|         name: Get changed files |         name: Get changed files | ||||||
|         id: changed-files-specific |         id: changed-files-specific | ||||||
|         uses: tj-actions/changed-files@v19 |         uses: tj-actions/changed-files@v23.1 | ||||||
|         with: |         with: | ||||||
|           files: | |           files: | | ||||||
|             src/** |             src/** | ||||||
|   | |||||||
| @@ -28,20 +28,20 @@ jobs: | |||||||
|         uses: actions/checkout@v3 |         uses: actions/checkout@v3 | ||||||
|       - |       - | ||||||
|         name: Login to Github Container Registry |         name: Login to Github Container Registry | ||||||
|         uses: docker/login-action@v1 |         uses: docker/login-action@v2 | ||||||
|         with: |         with: | ||||||
|           registry: ghcr.io |           registry: ghcr.io | ||||||
|           username: ${{ github.actor }} |           username: ${{ github.actor }} | ||||||
|           password: ${{ secrets.GITHUB_TOKEN }} |           password: ${{ secrets.GITHUB_TOKEN }} | ||||||
|       - |       - | ||||||
|         name: Set up Docker Buildx |         name: Set up Docker Buildx | ||||||
|         uses: docker/setup-buildx-action@v1 |         uses: docker/setup-buildx-action@v2 | ||||||
|       - |       - | ||||||
|         name: Set up QEMU |         name: Set up QEMU | ||||||
|         uses: docker/setup-qemu-action@v1 |         uses: docker/setup-qemu-action@v2 | ||||||
|       - |       - | ||||||
|         name: Build ${{ fromJSON(inputs.build-json).name }} |         name: Build ${{ fromJSON(inputs.build-json).name }} | ||||||
|         uses: docker/build-push-action@v2 |         uses: docker/build-push-action@v3 | ||||||
|         with: |         with: | ||||||
|           context: . |           context: . | ||||||
|           file: ${{ inputs.dockerfile }} |           file: ${{ inputs.dockerfile }} | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -70,6 +70,7 @@ target/ | |||||||
| .virtualenv | .virtualenv | ||||||
| virtualenv | virtualenv | ||||||
| /venv | /venv | ||||||
|  | .venv/ | ||||||
| /docker-compose.env | /docker-compose.env | ||||||
| /docker-compose.yml | /docker-compose.yml | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ | |||||||
| repos: | repos: | ||||||
|   # General hooks |   # General hooks | ||||||
|   - repo: https://github.com/pre-commit/pre-commit-hooks |   - repo: https://github.com/pre-commit/pre-commit-hooks | ||||||
|     rev: v4.2.0 |     rev: v4.3.0 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: check-docstring-first |       - id: check-docstring-first | ||||||
|       - id: check-json |       - id: check-json | ||||||
| @@ -27,7 +27,7 @@ repos: | |||||||
|       - id: check-case-conflict |       - id: check-case-conflict | ||||||
|       - id: detect-private-key |       - id: detect-private-key | ||||||
|   - repo: https://github.com/pre-commit/mirrors-prettier |   - repo: https://github.com/pre-commit/mirrors-prettier | ||||||
|     rev: "v2.6.2" |     rev: "v2.7.1" | ||||||
|     hooks: |     hooks: | ||||||
|       - id: prettier |       - id: prettier | ||||||
|         types_or: |         types_or: | ||||||
| @@ -37,7 +37,7 @@ repos: | |||||||
|         exclude: "(^Pipfile\\.lock$)" |         exclude: "(^Pipfile\\.lock$)" | ||||||
|   # Python hooks |   # Python hooks | ||||||
|   - repo: https://github.com/asottile/reorder_python_imports |   - repo: https://github.com/asottile/reorder_python_imports | ||||||
|     rev: v3.1.0 |     rev: v3.8.1 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: reorder-python-imports |       - id: reorder-python-imports | ||||||
|         exclude: "(migrations)" |         exclude: "(migrations)" | ||||||
| @@ -59,11 +59,11 @@ repos: | |||||||
|         args: |         args: | ||||||
|           - "--config=./src/setup.cfg" |           - "--config=./src/setup.cfg" | ||||||
|   - repo: https://github.com/psf/black |   - repo: https://github.com/psf/black | ||||||
|     rev: 22.3.0 |     rev: 22.6.0 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: black |       - id: black | ||||||
|   - repo: https://github.com/asottile/pyupgrade |   - repo: https://github.com/asottile/pyupgrade | ||||||
|     rev: v2.32.1 |     rev: v2.37.1 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: pyupgrade |       - id: pyupgrade | ||||||
|         exclude: "(migrations)" |         exclude: "(migrations)" | ||||||
|   | |||||||
							
								
								
									
										16
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						| @@ -77,15 +77,12 @@ ARG RUNTIME_PACKAGES="\ | |||||||
|   libraqm0 \ |   libraqm0 \ | ||||||
|   libgnutls30 \ |   libgnutls30 \ | ||||||
|   libjpeg62-turbo \ |   libjpeg62-turbo \ | ||||||
|   optipng \ |  | ||||||
|   python3 \ |   python3 \ | ||||||
|   python3-pip \ |   python3-pip \ | ||||||
|   python3-setuptools \ |   python3-setuptools \ | ||||||
|   postgresql-client \ |   postgresql-client \ | ||||||
|   # For Numpy |   # For Numpy | ||||||
|   libatlas3-base \ |   libatlas3-base \ | ||||||
|   # thumbnail size reduction |  | ||||||
|   pngquant \ |  | ||||||
|   # OCRmyPDF dependencies |   # OCRmyPDF dependencies | ||||||
|   tesseract-ocr \ |   tesseract-ocr \ | ||||||
|   tesseract-ocr-eng \ |   tesseract-ocr-eng \ | ||||||
| @@ -151,14 +148,14 @@ RUN --mount=type=bind,from=qpdf-builder,target=/qpdf \ | |||||||
|     && apt-get install --yes --no-install-recommends /qpdf/usr/src/qpdf/libqpdf28_*.deb \ |     && apt-get install --yes --no-install-recommends /qpdf/usr/src/qpdf/libqpdf28_*.deb \ | ||||||
|     && apt-get install --yes --no-install-recommends /qpdf/usr/src/qpdf/qpdf_*.deb \ |     && apt-get install --yes --no-install-recommends /qpdf/usr/src/qpdf/qpdf_*.deb \ | ||||||
|   && echo "Installing pikepdf and dependencies" \ |   && echo "Installing pikepdf and dependencies" \ | ||||||
|     && python3 -m pip install --no-cache-dir /pikepdf/usr/src/pikepdf/wheels/packaging*.whl \ |     && python3 -m pip install --no-cache-dir /pikepdf/usr/src/wheels/packaging*.whl \ | ||||||
|     && python3 -m pip install --no-cache-dir /pikepdf/usr/src/pikepdf/wheels/lxml*.whl \ |     && python3 -m pip install --no-cache-dir /pikepdf/usr/src/wheels/lxml*.whl \ | ||||||
|     && python3 -m pip install --no-cache-dir /pikepdf/usr/src/pikepdf/wheels/Pillow*.whl \ |     && python3 -m pip install --no-cache-dir /pikepdf/usr/src/wheels/Pillow*.whl \ | ||||||
|     && python3 -m pip install --no-cache-dir /pikepdf/usr/src/pikepdf/wheels/pyparsing*.whl \ |     && python3 -m pip install --no-cache-dir /pikepdf/usr/src/wheels/pyparsing*.whl \ | ||||||
|     && python3 -m pip install --no-cache-dir /pikepdf/usr/src/pikepdf/wheels/pikepdf*.whl \ |     && python3 -m pip install --no-cache-dir /pikepdf/usr/src/wheels/pikepdf*.whl \ | ||||||
|     && python -m pip list \ |     && python -m pip list \ | ||||||
|   && echo "Installing psycopg2" \ |   && echo "Installing psycopg2" \ | ||||||
|     && python3 -m pip install --no-cache-dir /psycopg2/usr/src/psycopg2/wheels/psycopg2*.whl \ |     && python3 -m pip install --no-cache-dir /psycopg2/usr/src/wheels/psycopg2*.whl \ | ||||||
|     && python -m pip list |     && python -m pip list | ||||||
|  |  | ||||||
| # Python dependencies | # Python dependencies | ||||||
| @@ -169,6 +166,7 @@ COPY requirements.txt ../ | |||||||
| # dependencies | # dependencies | ||||||
| ARG BUILD_PACKAGES="\ | ARG BUILD_PACKAGES="\ | ||||||
|   build-essential \ |   build-essential \ | ||||||
|  |   git \ | ||||||
|   python3-dev" |   python3-dev" | ||||||
|  |  | ||||||
| RUN set -eux \ | RUN set -eux \ | ||||||
|   | |||||||
							
								
								
									
										17
									
								
								Pipfile
									
									
									
									
									
								
							
							
						
						| @@ -13,8 +13,8 @@ dateparser = "~=1.1" | |||||||
| django = "~=4.0" | django = "~=4.0" | ||||||
| django-cors-headers = "*" | django-cors-headers = "*" | ||||||
| django-extensions = "*" | django-extensions = "*" | ||||||
| django-filter = "~=21.1" | django-filter = "~=22.1" | ||||||
| django-q = "~=1.3" | django-q = {editable = true, ref = "paperless-main", git = "https://github.com/paperless-ngx/django-q.git"} | ||||||
| djangorestframework = "~=3.13" | djangorestframework = "~=3.13" | ||||||
| filelock = "*" | filelock = "*" | ||||||
| fuzzywuzzy = {extras = ["speedup"], version = "*"} | fuzzywuzzy = {extras = ["speedup"], version = "*"} | ||||||
| @@ -22,20 +22,17 @@ gunicorn = "*" | |||||||
| imap-tools = "*" | imap-tools = "*" | ||||||
| langdetect = "*" | langdetect = "*" | ||||||
| pathvalidate = "*" | pathvalidate = "*" | ||||||
| pillow = "~=9.1" | pillow = "~=9.2" | ||||||
| # Any version update to pikepdf requires a base image update |  | ||||||
| pikepdf = "~=5.1" | pikepdf = "~=5.1" | ||||||
| python-gnupg = "*" | python-gnupg = "*" | ||||||
| python-dotenv = "*" | python-dotenv = "*" | ||||||
| python-dateutil = "*" | python-dateutil = "*" | ||||||
| python-magic = "*" | python-magic = "*" | ||||||
| # Any version update to psycopg2 requires a base image update |  | ||||||
| psycopg2 = "*" | psycopg2 = "*" | ||||||
| redis = "*" | redis = "*" | ||||||
| # Pinned because aarch64 wheels and updates cause warnings when loading the classifier model. | scikit-learn="~=1.1" | ||||||
| scikit-learn="==1.0.2" | whitenoise = "~=6.2.0" | ||||||
| whitenoise = "~=6.0.0" | watchdog = "~=2.1.9" | ||||||
| watchdog = "~=2.1.0" |  | ||||||
| whoosh="~=2.7.4" | whoosh="~=2.7.4" | ||||||
| inotifyrecursive = "~=0.3" | inotifyrecursive = "~=0.3" | ||||||
| ocrmypdf = "~=13.4" | ocrmypdf = "~=13.4" | ||||||
| @@ -65,7 +62,7 @@ pytest-django = "*" | |||||||
| pytest-env = "*" | pytest-env = "*" | ||||||
| pytest-sugar = "*" | pytest-sugar = "*" | ||||||
| pytest-xdist = "*" | pytest-xdist = "*" | ||||||
| sphinx = "~=4.5.0" | sphinx = "~=5.0.2" | ||||||
| sphinx_rtd_theme = "*" | sphinx_rtd_theme = "*" | ||||||
| tox = "*" | tox = "*" | ||||||
| black = "*" | black = "*" | ||||||
|   | |||||||
							
								
								
									
										1398
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						| @@ -34,6 +34,7 @@ branch_name=$(git rev-parse --abbrev-ref HEAD) | |||||||
| export DOCKER_BUILDKIT=1 | export DOCKER_BUILDKIT=1 | ||||||
|  |  | ||||||
| docker build --file "$1" \ | docker build --file "$1" \ | ||||||
|  | 	--progress=plain \ | ||||||
| 	--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/app:"${branch_name}" \ | 	--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/app:"${branch_name}" \ | ||||||
| 	--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/app:dev \ | 	--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/app:dev \ | ||||||
| 	--build-arg JBIG2ENC_VERSION="${jbig2enc_version}" \ | 	--build-arg JBIG2ENC_VERSION="${jbig2enc_version}" \ | ||||||
|   | |||||||
| @@ -2,8 +2,7 @@ | |||||||
| # Inputs: | # Inputs: | ||||||
| #    - REPO - Docker repository to pull qpdf from | #    - REPO - Docker repository to pull qpdf from | ||||||
| #    - QPDF_VERSION - The image qpdf version to copy .deb files from | #    - QPDF_VERSION - The image qpdf version to copy .deb files from | ||||||
| #    - PIKEPDF_GIT_TAG - The Git tag to clone and build from | #    - PIKEPDF_VERSION - Version of pikepdf to build wheel for | ||||||
| #    - PIKEPDF_VERSION - Used to force the built pikepdf version to match |  | ||||||
|  |  | ||||||
| # Default to pulling from the main repo registry when manually building | # Default to pulling from the main repo registry when manually building | ||||||
| ARG REPO="paperless-ngx/paperless-ngx" | ARG REPO="paperless-ngx/paperless-ngx" | ||||||
| @@ -23,7 +22,6 @@ ARG BUILD_PACKAGES="\ | |||||||
|   build-essential \ |   build-essential \ | ||||||
|   python3-dev \ |   python3-dev \ | ||||||
|   python3-pip \ |   python3-pip \ | ||||||
|   git \ |  | ||||||
|   # qpdf requirement - https://github.com/qpdf/qpdf#crypto-providers |   # qpdf requirement - https://github.com/qpdf/qpdf#crypto-providers | ||||||
|   libgnutls28-dev \ |   libgnutls28-dev \ | ||||||
|   # lxml requrements - https://lxml.de/installation.html |   # lxml requrements - https://lxml.de/installation.html | ||||||
| @@ -72,21 +70,19 @@ RUN set -eux \ | |||||||
| # For better caching, seperate the basic installs from | # For better caching, seperate the basic installs from | ||||||
| # the building | # the building | ||||||
|  |  | ||||||
| ARG PIKEPDF_GIT_TAG |  | ||||||
| ARG PIKEPDF_VERSION | ARG PIKEPDF_VERSION | ||||||
|  |  | ||||||
| RUN set -eux \ | RUN set -eux \ | ||||||
|   && echo "building pikepdf wheel" \ |   && echo "Building pikepdf wheel ${PIKEPDF_VERSION}" \ | ||||||
|   # Note the v in the tag name here |  | ||||||
|   && git clone --quiet --depth 1 --branch "${PIKEPDF_GIT_TAG}" https://github.com/pikepdf/pikepdf.git \ |  | ||||||
|   && cd pikepdf \ |  | ||||||
|   # pikepdf seems to specifciy either a next version when built OR |  | ||||||
|   # a post release tag. |  | ||||||
|   # In either case, this won't match what we want from requirements.txt |  | ||||||
|   # Directly modify the setup.py to set the version we just checked out of Git |  | ||||||
|   && sed -i "s/use_scm_version=True/version=\"${PIKEPDF_VERSION}\"/g" setup.py \ |  | ||||||
|   # https://github.com/pikepdf/pikepdf/issues/323 |  | ||||||
|   && rm pyproject.toml \ |  | ||||||
|   && mkdir wheels \ |   && mkdir wheels \ | ||||||
|   && python3 -m pip wheel . --wheel-dir wheels \ |   && python3 -m pip wheel \ | ||||||
|  |     # Build the package at the required version | ||||||
|  |     pikepdf==${PIKEPDF_VERSION} \ | ||||||
|  |     # Output the *.whl into this directory | ||||||
|  |     --wheel-dir wheels \ | ||||||
|  |     # Do not use a binary packge for the package being built | ||||||
|  |     --no-binary=pikepdf \ | ||||||
|  |     # Do use binary packages for dependencies | ||||||
|  |     --prefer-binary \ | ||||||
|  |     --no-cache-dir \ | ||||||
|   && ls -ahl wheels |   && ls -ahl wheels | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| # This Dockerfile builds the psycopg2 wheel | # This Dockerfile builds the psycopg2 wheel | ||||||
| # Inputs: | # Inputs: | ||||||
| #    - PSYCOPG2_GIT_TAG - The Git tag to clone and build from | #    - PSYCOPG2_VERSION - Version to build | ||||||
| #    - PSYCOPG2_VERSION - Unused, kept for future possible usage |  | ||||||
|  |  | ||||||
| FROM python:3.9-slim-bullseye as main | FROM python:3.9-slim-bullseye as main | ||||||
|  |  | ||||||
| @@ -11,7 +10,6 @@ ARG DEBIAN_FRONTEND=noninteractive | |||||||
|  |  | ||||||
| ARG BUILD_PACKAGES="\ | ARG BUILD_PACKAGES="\ | ||||||
|   build-essential \ |   build-essential \ | ||||||
|   git \ |  | ||||||
|   python3-dev \ |   python3-dev \ | ||||||
|   python3-pip \ |   python3-pip \ | ||||||
|   # https://www.psycopg.org/docs/install.html#prerequisites |   # https://www.psycopg.org/docs/install.html#prerequisites | ||||||
| @@ -32,14 +30,20 @@ RUN set -eux \ | |||||||
| # For better caching, seperate the basic installs from | # For better caching, seperate the basic installs from | ||||||
| # the building | # the building | ||||||
|  |  | ||||||
| ARG PSYCOPG2_GIT_TAG |  | ||||||
| ARG PSYCOPG2_VERSION | ARG PSYCOPG2_VERSION | ||||||
|  |  | ||||||
| RUN set -eux \ | RUN set -eux \ | ||||||
|   && echo "Building psycopg2 wheel" \ |   && echo "Building psycopg2 wheel ${PSYCOPG2_VERSION}" \ | ||||||
|   && cd /usr/src \ |   && cd /usr/src \ | ||||||
|   && git clone --quiet --depth 1 --branch ${PSYCOPG2_GIT_TAG} https://github.com/psycopg/psycopg2.git \ |  | ||||||
|   && cd psycopg2 \ |  | ||||||
|   && mkdir wheels \ |   && mkdir wheels \ | ||||||
|   && python3 -m pip wheel . --wheel-dir wheels \ |   && python3 -m pip wheel \ | ||||||
|  |     # Build the package at the required version | ||||||
|  |     psycopg2==${PSYCOPG2_VERSION} \ | ||||||
|  |     # Output the *.whl into this directory | ||||||
|  |     --wheel-dir wheels \ | ||||||
|  |     # Do not use a binary packge for the package being built | ||||||
|  |     --no-binary=psycopg2 \ | ||||||
|  |     # Do use binary packages for dependencies | ||||||
|  |     --prefer-binary \ | ||||||
|  |     --no-cache-dir \ | ||||||
|   && ls -ahl wheels/ |   && ls -ahl wheels/ | ||||||
|   | |||||||
| @@ -31,13 +31,13 @@ | |||||||
| version: "3.4" | version: "3.4" | ||||||
| services: | services: | ||||||
|   broker: |   broker: | ||||||
|     image: redis:6.0 |     image: docker.io/library/redis:6.0 | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     volumes: |     volumes: | ||||||
|       - redisdata:/data |       - redisdata:/data | ||||||
|  |  | ||||||
|   db: |   db: | ||||||
|     image: postgres:13 |     image: docker.io/library/postgres:13 | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     volumes: |     volumes: | ||||||
|       - pgdata:/var/lib/postgresql/data |       - pgdata:/var/lib/postgresql/data | ||||||
|   | |||||||
| @@ -33,13 +33,13 @@ | |||||||
| version: "3.4" | version: "3.4" | ||||||
| services: | services: | ||||||
|   broker: |   broker: | ||||||
|     image: redis:6.0 |     image: docker.io/library/redis:6.0 | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     volumes: |     volumes: | ||||||
|       - redisdata:/data |       - redisdata:/data | ||||||
|  |  | ||||||
|   db: |   db: | ||||||
|     image: postgres:13 |     image: docker.io/library/postgres:13 | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     volumes: |     volumes: | ||||||
|       - pgdata:/var/lib/postgresql/data |       - pgdata:/var/lib/postgresql/data | ||||||
| @@ -77,7 +77,7 @@ services: | |||||||
|       PAPERLESS_TIKA_ENDPOINT: http://tika:9998 |       PAPERLESS_TIKA_ENDPOINT: http://tika:9998 | ||||||
|  |  | ||||||
|   gotenberg: |   gotenberg: | ||||||
|     image: gotenberg/gotenberg:7.4 |     image: docker.io/gotenberg/gotenberg:7.4 | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|  |  | ||||||
|   tika: |   tika: | ||||||
|   | |||||||
| @@ -29,13 +29,13 @@ | |||||||
| version: "3.4" | version: "3.4" | ||||||
| services: | services: | ||||||
|   broker: |   broker: | ||||||
|     image: redis:6.0 |     image: docker.io/library/redis:6.0 | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     volumes: |     volumes: | ||||||
|       - redisdata:/data |       - redisdata:/data | ||||||
|  |  | ||||||
|   db: |   db: | ||||||
|     image: postgres:13 |     image: docker.io/library/postgres:13 | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     volumes: |     volumes: | ||||||
|       - pgdata:/var/lib/postgresql/data |       - pgdata:/var/lib/postgresql/data | ||||||
|   | |||||||
| @@ -33,7 +33,7 @@ | |||||||
| version: "3.4" | version: "3.4" | ||||||
| services: | services: | ||||||
|   broker: |   broker: | ||||||
|     image: redis:6.0 |     image: docker.io/library/redis:6.0 | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     volumes: |     volumes: | ||||||
|       - redisdata:/data |       - redisdata:/data | ||||||
| @@ -65,7 +65,7 @@ services: | |||||||
|       PAPERLESS_TIKA_ENDPOINT: http://tika:9998 |       PAPERLESS_TIKA_ENDPOINT: http://tika:9998 | ||||||
|  |  | ||||||
|   gotenberg: |   gotenberg: | ||||||
|     image: gotenberg/gotenberg:7.4 |     image: docker.io/gotenberg/gotenberg:7.4 | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|  |  | ||||||
|   tika: |   tika: | ||||||
|   | |||||||
| @@ -26,7 +26,7 @@ | |||||||
| version: "3.4" | version: "3.4" | ||||||
| services: | services: | ||||||
|   broker: |   broker: | ||||||
|     image: redis:6.0 |     image: docker.io/library/redis:6.0 | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     volumes: |     volumes: | ||||||
|       - redisdata:/data |       - redisdata:/data | ||||||
|   | |||||||
| @@ -2,6 +2,37 @@ | |||||||
|  |  | ||||||
| set -e | set -e | ||||||
|  |  | ||||||
|  | # Adapted from: | ||||||
|  | # https://github.com/docker-library/postgres/blob/master/docker-entrypoint.sh | ||||||
|  | # usage: file_env VAR | ||||||
|  | #    ie: file_env 'XYZ_DB_PASSWORD' will allow for "$XYZ_DB_PASSWORD_FILE" to | ||||||
|  | # fill in the value of "$XYZ_DB_PASSWORD" from a file, especially for Docker's | ||||||
|  | # secrets feature | ||||||
|  | file_env() { | ||||||
|  | 	local var="$1" | ||||||
|  | 	local fileVar="${var}_FILE" | ||||||
|  |  | ||||||
|  | 	# Basic validation | ||||||
|  | 	if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then | ||||||
|  | 		echo >&2 "error: both $var and $fileVar are set (but are exclusive)" | ||||||
|  | 		exit 1 | ||||||
|  | 	fi | ||||||
|  |  | ||||||
|  | 	# Only export var if the _FILE exists | ||||||
|  | 	if [ "${!fileVar:-}" ]; then | ||||||
|  | 		# And the file exists | ||||||
|  | 		if [[ -f ${!fileVar} ]]; then | ||||||
|  | 			echo "Setting ${var} from file" | ||||||
|  | 			val="$(< "${!fileVar}")" | ||||||
|  | 			export "$var"="$val" | ||||||
|  | 		else | ||||||
|  | 			echo "File ${!fileVar} doesn't exist" | ||||||
|  | 			exit 1 | ||||||
|  | 		fi | ||||||
|  | 	fi | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
| # Source: https://github.com/sameersbn/docker-gitlab/ | # Source: https://github.com/sameersbn/docker-gitlab/ | ||||||
| map_uidgid() { | map_uidgid() { | ||||||
| 	USERMAP_ORIG_UID=$(id -u paperless) | 	USERMAP_ORIG_UID=$(id -u paperless) | ||||||
| @@ -15,23 +46,53 @@ map_uidgid() { | |||||||
| 	fi | 	fi | ||||||
| } | } | ||||||
|  |  | ||||||
|  | map_folders() { | ||||||
|  | 	# Export these so they can be used in docker-prepare.sh | ||||||
|  | 	export DATA_DIR="${PAPERLESS_DATA_DIR:-/usr/src/paperless/data}" | ||||||
|  | 	export MEDIA_ROOT_DIR="${PAPERLESS_MEDIA_ROOT:-/usr/src/paperless/media}" | ||||||
|  | } | ||||||
|  |  | ||||||
| initialize() { | initialize() { | ||||||
|  |  | ||||||
|  | 	# Setup environment from secrets before anything else | ||||||
|  | 	for env_var in \ | ||||||
|  | 		PAPERLESS_DBUSER \ | ||||||
|  | 		PAPERLESS_DBPASS \ | ||||||
|  | 		PAPERLESS_SECRET_KEY \ | ||||||
|  | 		PAPERLESS_AUTO_LOGIN_USERNAME \ | ||||||
|  | 		PAPERLESS_ADMIN_USER \ | ||||||
|  | 		PAPERLESS_ADMIN_MAIL \ | ||||||
|  | 		PAPERLESS_ADMIN_PASSWORD; do | ||||||
|  | 		# Check for a version of this var with _FILE appended | ||||||
|  | 		# and convert the contents to the env var value | ||||||
|  | 		file_env ${env_var} | ||||||
|  | 	done | ||||||
|  |  | ||||||
|  | 	# Change the user and group IDs if needed | ||||||
| 	map_uidgid | 	map_uidgid | ||||||
|  |  | ||||||
| 	for dir in export data data/index media media/documents media/documents/originals media/documents/thumbnails; do | 	# Check for overrides of certain folders | ||||||
| 		if [[ ! -d "../$dir" ]]; then | 	map_folders | ||||||
| 			echo "Creating directory ../$dir" |  | ||||||
| 			mkdir ../$dir | 	local export_dir="/usr/src/paperless/export" | ||||||
|  |  | ||||||
|  | 	for dir in "${export_dir}" "${DATA_DIR}" "${DATA_DIR}/index" "${MEDIA_ROOT_DIR}" "${MEDIA_ROOT_DIR}/documents" "${MEDIA_ROOT_DIR}/documents/originals" "${MEDIA_ROOT_DIR}/documents/thumbnails"; do | ||||||
|  | 		if [[ ! -d "${dir}" ]]; then | ||||||
|  | 			echo "Creating directory ${dir}" | ||||||
|  | 			mkdir "${dir}" | ||||||
| 		fi | 		fi | ||||||
| 	done | 	done | ||||||
|  |  | ||||||
| 	echo "Creating directory /tmp/paperless" | 	local tmp_dir="/tmp/paperless" | ||||||
| 	mkdir -p /tmp/paperless | 	echo "Creating directory ${tmp_dir}" | ||||||
|  | 	mkdir -p "${tmp_dir}" | ||||||
|  |  | ||||||
| 	set +e | 	set +e | ||||||
| 	echo "Adjusting permissions of paperless files. This may take a while." | 	echo "Adjusting permissions of paperless files. This may take a while." | ||||||
| 	chown -R paperless:paperless /tmp/paperless | 	chown -R paperless:paperless ${tmp_dir} | ||||||
| 	find .. -not \( -user paperless -and -group paperless \) -exec chown paperless:paperless {} + | 	for dir in "${export_dir}" "${DATA_DIR}" "${MEDIA_ROOT_DIR}"; do | ||||||
|  | 		find "${dir}" -not \( -user paperless -and -group paperless \) -exec chown paperless:paperless {} + | ||||||
|  | 	done | ||||||
| 	set -e | 	set -e | ||||||
|  |  | ||||||
| 	gosu paperless /sbin/docker-prepare.sh | 	gosu paperless /sbin/docker-prepare.sh | ||||||
|   | |||||||
| @@ -3,16 +3,17 @@ | |||||||
| set -e | set -e | ||||||
|  |  | ||||||
| wait_for_postgres() { | wait_for_postgres() { | ||||||
| 	attempt_num=1 | 	local attempt_num=1 | ||||||
| 	max_attempts=5 | 	local max_attempts=5 | ||||||
|  |  | ||||||
| 	echo "Waiting for PostgreSQL to start..." | 	echo "Waiting for PostgreSQL to start..." | ||||||
|  |  | ||||||
| 	host="${PAPERLESS_DBHOST:=localhost}" | 	local host="${PAPERLESS_DBHOST:-localhost}" | ||||||
| 	port="${PAPERLESS_DBPORT:=5432}" | 	local port="${PAPERLESS_DBPORT:-5432}" | ||||||
|  |  | ||||||
|  | 	# Disable warning, host and port can't have spaces | ||||||
| 	while [ ! "$(pg_isready -h $host -p $port)" ]; do | 	# shellcheck disable=SC2086 | ||||||
|  | 	while [ ! "$(pg_isready -h ${host} -p ${port})" ]; do | ||||||
|  |  | ||||||
| 		if [ $attempt_num -eq $max_attempts ]; then | 		if [ $attempt_num -eq $max_attempts ]; then | ||||||
| 			echo "Unable to connect to database." | 			echo "Unable to connect to database." | ||||||
| @@ -43,17 +44,18 @@ migrations() { | |||||||
| 		flock 200 | 		flock 200 | ||||||
| 		echo "Apply database migrations..." | 		echo "Apply database migrations..." | ||||||
| 		python3 manage.py migrate | 		python3 manage.py migrate | ||||||
| 	) 200>/usr/src/paperless/data/migration_lock | 	) 200>"${DATA_DIR}/migration_lock" | ||||||
| } | } | ||||||
|  |  | ||||||
| search_index() { | search_index() { | ||||||
| 	index_version=1 |  | ||||||
| 	index_version_file=/usr/src/paperless/data/.index_version |  | ||||||
|  |  | ||||||
| 	if [[ (! -f "$index_version_file") || $(<$index_version_file) != "$index_version" ]]; then | 	local index_version=1 | ||||||
|  | 	local index_version_file=${DATA_DIR}/.index_version | ||||||
|  |  | ||||||
|  | 	if [[ (! -f "${index_version_file}") || $(<"${index_version_file}") != "$index_version" ]]; then | ||||||
| 		echo "Search index out of date. Updating..." | 		echo "Search index out of date. Updating..." | ||||||
| 		python3 manage.py document_index reindex --no-progress-bar | 		python3 manage.py document_index reindex --no-progress-bar | ||||||
| 		echo $index_version | tee $index_version_file >/dev/null | 		echo ${index_version} | tee "${index_version_file}" >/dev/null | ||||||
| 	fi | 	fi | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,7 +2,18 @@ | |||||||
|  |  | ||||||
| set -eu | set -eu | ||||||
|  |  | ||||||
| for command in document_archiver document_exporter document_importer mail_fetcher document_create_classifier document_index document_renamer document_retagger document_thumbnails document_sanity_checker manage_superuser; | for command in decrypt_documents \ | ||||||
|  | 	document_archiver \ | ||||||
|  | 	document_exporter \ | ||||||
|  | 	document_importer \ | ||||||
|  | 	mail_fetcher \ | ||||||
|  | 	document_create_classifier \ | ||||||
|  | 	document_index \ | ||||||
|  | 	document_renamer \ | ||||||
|  | 	document_retagger \ | ||||||
|  | 	document_thumbnails \ | ||||||
|  | 	document_sanity_checker \ | ||||||
|  | 	manage_superuser; | ||||||
| do | do | ||||||
| 	echo "installing $command..." | 	echo "installing $command..." | ||||||
| 	sed "s/management_command/$command/g" management_script.sh > /usr/local/bin/$command | 	sed "s/management_command/$command/g" management_script.sh > /usr/local/bin/$command | ||||||
|   | |||||||
| @@ -26,9 +26,11 @@ if __name__ == "__main__": | |||||||
|             try: |             try: | ||||||
|                 client.ping() |                 client.ping() | ||||||
|                 break |                 break | ||||||
|             except Exception: |             except Exception as e: | ||||||
|                 print( |                 print( | ||||||
|                     f"Redis ping #{attempt} failed, waiting {RETRY_SLEEP_SECONDS}s", |                     f"Redis ping #{attempt} failed.\n" | ||||||
|  |                     f"Error: {str(e)}.\n" | ||||||
|  |                     f"Waiting {RETRY_SLEEP_SECONDS}s", | ||||||
|                     flush=True, |                     flush=True, | ||||||
|                 ) |                 ) | ||||||
|                 time.sleep(RETRY_SLEEP_SECONDS) |                 time.sleep(RETRY_SLEEP_SECONDS) | ||||||
|   | |||||||
| @@ -31,7 +31,8 @@ The objects served by the document endpoint contain the following fields: | |||||||
| *   ``tags``: List of IDs of tags assigned to this document, or empty list. | *   ``tags``: List of IDs of tags assigned to this document, or empty list. | ||||||
| *   ``document_type``: Document type of this document, or null. | *   ``document_type``: Document type of this document, or null. | ||||||
| *   ``correspondent``:  Correspondent of this document or null. | *   ``correspondent``:  Correspondent of this document or null. | ||||||
| *   ``created``: The date at which this document was created. | *   ``created``: The date time at which this document was created. | ||||||
|  | *   ``created_date``: The date (YYYY-MM-DD) at which this document was created. Optional. If also passed with created, this is ignored. | ||||||
| *   ``modified``: The date at which this document was last edited in paperless. Read-only. | *   ``modified``: The date at which this document was last edited in paperless. Read-only. | ||||||
| *   ``added``: The date at which this document was added to paperless. Read-only. | *   ``added``: The date at which this document was added to paperless. Read-only. | ||||||
| *   ``archive_serial_number``: The identifier of this document in a physical document archive. | *   ``archive_serial_number``: The identifier of this document in a physical document archive. | ||||||
|   | |||||||
| @@ -424,14 +424,23 @@ PAPERLESS_OCR_IMAGE_DPI=<num> | |||||||
|     the produced PDF documents are A4 sized. |     the produced PDF documents are A4 sized. | ||||||
|  |  | ||||||
| PAPERLESS_OCR_MAX_IMAGE_PIXELS=<num> | PAPERLESS_OCR_MAX_IMAGE_PIXELS=<num> | ||||||
|     Paperless will not OCR images that have more pixels than this limit. |     Paperless will raise a warning when OCRing images which are over this limit and | ||||||
|     This is intended to prevent decompression bombs from overloading paperless. |     will not OCR images which are more than twice this limit.  Note this does not | ||||||
|     Increasing this limit is desired if you face a DecompressionBombError despite |     prevent the document from being consumed, but could result in missing text content. | ||||||
|     the concerning file not being malicious; this could e.g. be caused by invalidly |  | ||||||
|     recognized metadata. |     If unset, will default to the value determined by | ||||||
|     If you have enough resources or if you are certain that your uploaded files |     `Pillow <https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.MAX_IMAGE_PIXELS>`_. | ||||||
|     are not malicious you can increase this value to your needs. |  | ||||||
|     The default value is 256000000, an image with more pixels than that would not be parsed. |     .. note:: | ||||||
|  |  | ||||||
|  |         Increasing this limit could cause Paperless to consume additional resources | ||||||
|  |         when consuming a file.  Be sure you have sufficient system resources. | ||||||
|  |  | ||||||
|  |     .. caution:: | ||||||
|  |  | ||||||
|  |         The limit is intended to prevent malicious files from consuming system resources | ||||||
|  |         and causing crashes and other errors.  Only increase this value if you are certain | ||||||
|  |         your documents are not malicious and you need the text which was not OCRed | ||||||
|  |  | ||||||
| PAPERLESS_OCR_USER_ARGS=<json> | PAPERLESS_OCR_USER_ARGS=<json> | ||||||
|     OCRmyPDF offers many more options. Use this parameter to specify any |     OCRmyPDF offers many more options. Use this parameter to specify any | ||||||
| @@ -700,13 +709,6 @@ PAPERLESS_CONVERT_TMPDIR=<path> | |||||||
|  |  | ||||||
|     Default is none, which disables the temporary directory. |     Default is none, which disables the temporary directory. | ||||||
|  |  | ||||||
| PAPERLESS_OPTIMIZE_THUMBNAILS=<bool> |  | ||||||
|     Use optipng to optimize thumbnails. This usually reduces the size of |  | ||||||
|     thumbnails by about 20%, but uses considerable compute time during |  | ||||||
|     consumption. |  | ||||||
|  |  | ||||||
|     Defaults to true. |  | ||||||
|  |  | ||||||
| PAPERLESS_POST_CONSUME_SCRIPT=<filename> | PAPERLESS_POST_CONSUME_SCRIPT=<filename> | ||||||
|     After a document is consumed, Paperless can trigger an arbitrary script if |     After a document is consumed, Paperless can trigger an arbitrary script if | ||||||
|     you like.  This script will be passed a number of arguments for you to work |     you like.  This script will be passed a number of arguments for you to work | ||||||
| @@ -777,9 +779,6 @@ PAPERLESS_CONVERT_BINARY=<path> | |||||||
| PAPERLESS_GS_BINARY=<path> | PAPERLESS_GS_BINARY=<path> | ||||||
|     Defaults to "/usr/bin/gs". |     Defaults to "/usr/bin/gs". | ||||||
|  |  | ||||||
| PAPERLESS_OPTIPNG_BINARY=<path> |  | ||||||
|     Defaults to "/usr/bin/optipng". |  | ||||||
|  |  | ||||||
|  |  | ||||||
| .. _configuration-docker: | .. _configuration-docker: | ||||||
|  |  | ||||||
|   | |||||||
| @@ -200,6 +200,19 @@ Install Paperless from Docker Hub | |||||||
|         You can copy any setting from the file ``paperless.conf.example`` and paste it here. |         You can copy any setting from the file ``paperless.conf.example`` and paste it here. | ||||||
|         Have a look at :ref:`configuration` to see what's available. |         Have a look at :ref:`configuration` to see what's available. | ||||||
|  |  | ||||||
|  |     .. note:: | ||||||
|  |  | ||||||
|  |         You can utilize Docker secrets for some configuration settings by | ||||||
|  |         appending `_FILE` to some configuration values.  This is supported currently | ||||||
|  |         only by: | ||||||
|  |           * PAPERLESS_DBUSER | ||||||
|  |           * PAPERLESS_DBPASS | ||||||
|  |           * PAPERLESS_SECRET_KEY | ||||||
|  |           * PAPERLESS_AUTO_LOGIN_USERNAME | ||||||
|  |           * PAPERLESS_ADMIN_USER | ||||||
|  |           * PAPERLESS_ADMIN_MAIL | ||||||
|  |           * PAPERLESS_ADMIN_PASSWORD | ||||||
|  |  | ||||||
|     .. caution:: |     .. caution:: | ||||||
|  |  | ||||||
|         Some file systems such as NFS network shares don't support file system |         Some file systems such as NFS network shares don't support file system | ||||||
| @@ -286,7 +299,6 @@ writing. Windows is not and will never be supported. | |||||||
|  |  | ||||||
|     *   ``fonts-liberation`` for generating thumbnails for plain text files |     *   ``fonts-liberation`` for generating thumbnails for plain text files | ||||||
|     *   ``imagemagick`` >= 6 for PDF conversion |     *   ``imagemagick`` >= 6 for PDF conversion | ||||||
|     *   ``optipng`` for optimizing thumbnails |  | ||||||
|     *   ``gnupg`` for handling encrypted documents |     *   ``gnupg`` for handling encrypted documents | ||||||
|     *   ``libpq-dev`` for PostgreSQL |     *   ``libpq-dev`` for PostgreSQL | ||||||
|     *   ``libmagic-dev`` for mime type detection |     *   ``libmagic-dev`` for mime type detection | ||||||
| @@ -298,7 +310,7 @@ writing. Windows is not and will never be supported. | |||||||
|  |  | ||||||
|     .. code:: |     .. code:: | ||||||
|  |  | ||||||
|         python3 python3-pip python3-dev imagemagick fonts-liberation optipng gnupg libpq-dev libmagic-dev mime-support libzbar0 poppler-utils |         python3 python3-pip python3-dev imagemagick fonts-liberation gnupg libpq-dev libmagic-dev mime-support libzbar0 poppler-utils | ||||||
|  |  | ||||||
|     These dependencies are required for OCRmyPDF, which is used for text recognition. |     These dependencies are required for OCRmyPDF, which is used for text recognition. | ||||||
|  |  | ||||||
| @@ -730,8 +742,6 @@ configuring some options in paperless can help improve performance immensely: | |||||||
| *   If you want to perform OCR on the device, consider using ``PAPERLESS_OCR_CLEAN=none``. | *   If you want to perform OCR on the device, consider using ``PAPERLESS_OCR_CLEAN=none``. | ||||||
|     This will speed up OCR times and use less memory at the expense of slightly worse |     This will speed up OCR times and use less memory at the expense of slightly worse | ||||||
|     OCR results. |     OCR results. | ||||||
| *   Set ``PAPERLESS_OPTIMIZE_THUMBNAILS`` to 'false' if you want faster consumption |  | ||||||
|     times. Thumbnails will be about 20% larger. |  | ||||||
| *   If using docker, consider setting ``PAPERLESS_WEBSERVER_WORKERS`` to | *   If using docker, consider setting ``PAPERLESS_WEBSERVER_WORKERS`` to | ||||||
|     1. This will save some memory. |     1. This will save some memory. | ||||||
|  |  | ||||||
|   | |||||||
| @@ -161,6 +161,9 @@ These are as follows: | |||||||
|     will not consume flagged mails. |     will not consume flagged mails. | ||||||
| *   **Move to folder:** Moves consumed mails out of the way so that paperless wont | *   **Move to folder:** Moves consumed mails out of the way so that paperless wont | ||||||
|     consume them again. |     consume them again. | ||||||
|  | *   **Add custom Tag:** Adds a custom tag to mails with consumed documents (the IMAP | ||||||
|  |     standard calls these "keywords"). Paperless will not consume mails already tagged. | ||||||
|  |     Not all mail servers support this feature! | ||||||
|  |  | ||||||
| .. caution:: | .. caution:: | ||||||
|  |  | ||||||
|   | |||||||
| @@ -65,7 +65,6 @@ | |||||||
| #PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=false | #PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=false | ||||||
| #PAPERLESS_CONSUMER_ENABLE_BARCODES=false | #PAPERLESS_CONSUMER_ENABLE_BARCODES=false | ||||||
| #PAPERLESS_CONSUMER_ENABLE_BARCODES=PATCHT | #PAPERLESS_CONSUMER_ENABLE_BARCODES=PATCHT | ||||||
| #PAPERLESS_OPTIMIZE_THUMBNAILS=true |  | ||||||
| #PAPERLESS_PRE_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh | #PAPERLESS_PRE_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh | ||||||
| #PAPERLESS_POST_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh | #PAPERLESS_POST_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh | ||||||
| #PAPERLESS_FILENAME_DATE_ORDER=YMD | #PAPERLESS_FILENAME_DATE_ORDER=YMD | ||||||
| @@ -84,4 +83,3 @@ | |||||||
|  |  | ||||||
| #PAPERLESS_CONVERT_BINARY=/usr/bin/convert | #PAPERLESS_CONVERT_BINARY=/usr/bin/convert | ||||||
| #PAPERLESS_GS_BINARY=/usr/bin/gs | #PAPERLESS_GS_BINARY=/usr/bin/gs | ||||||
| #PAPERLESS_OPTIPNG_BINARY=/usr/bin/optipng |  | ||||||
|   | |||||||
| @@ -1,10 +1,3 @@ | |||||||
| # |  | ||||||
| # These requirements were autogenerated by pipenv |  | ||||||
| # To regenerate from the project's Pipfile, run: |  | ||||||
| # |  | ||||||
| #    pipenv lock --requirements |  | ||||||
| # |  | ||||||
|  |  | ||||||
| -i https://pypi.python.org/simple | -i https://pypi.python.org/simple | ||||||
| --extra-index-url https://www.piwheels.org/simple | --extra-index-url https://www.piwheels.org/simple | ||||||
| aioredis==1.3.1 | aioredis==1.3.1 | ||||||
| @@ -13,30 +6,32 @@ arrow==1.2.2; python_version >= '3.6' | |||||||
| asgiref==3.5.2; python_version >= '3.7' | asgiref==3.5.2; python_version >= '3.7' | ||||||
| async-timeout==4.0.2; python_version >= '3.6' | async-timeout==4.0.2; python_version >= '3.6' | ||||||
| attrs==21.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' | attrs==21.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' | ||||||
| autobahn==22.4.2; python_version >= '3.7' | autobahn==22.6.1; python_version >= '3.7' | ||||||
| automat==20.2.0 | automat==20.2.0 | ||||||
| backports.zoneinfo==0.2.1; python_version < '3.9' | backports.zoneinfo==0.2.1; python_version < '3.9' | ||||||
| blessed==1.19.1; python_version >= '2.7' | blessed==1.19.1; python_version >= '2.7' | ||||||
| certifi==2021.10.8 | certifi==2022.6.15; python_version >= '3.6' | ||||||
| cffi==1.15.0 | cffi==1.15.1 | ||||||
|  | channels==3.0.5 | ||||||
| channels-redis==3.4.0 | channels-redis==3.4.0 | ||||||
| channels==3.0.4 | charset-normalizer==2.1.0; python_version >= '3.6' | ||||||
| charset-normalizer==2.0.12; python_version >= '3.5' |  | ||||||
| click==8.1.3; python_version >= '3.7' | click==8.1.3; python_version >= '3.7' | ||||||
| coloredlogs==15.0.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' | coloredlogs==15.0.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' | ||||||
| concurrent-log-handler==0.9.20 | concurrent-log-handler==0.9.20 | ||||||
| constantly==15.1.0 | constantly==15.1.0 | ||||||
| cryptography==36.0.2; python_version >= '3.6' | cryptography==37.0.4; python_version >= '3.6' | ||||||
| daphne==3.0.2; python_version >= '3.6' | daphne==3.0.2; python_version >= '3.6' | ||||||
| dateparser==1.1.1 | dateparser==1.1.1 | ||||||
| django-cors-headers==3.12.0 | deprecated==1.2.13; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' | ||||||
| django-extensions==3.1.5 | deprecation==2.1.0 | ||||||
| django-filter==21.1 | django==4.0.6 | ||||||
| django-picklefield==3.0.1; python_version >= '3' | django-cors-headers==3.13.0 | ||||||
| django-q==1.3.9 | django-extensions==3.2.0 | ||||||
| django==4.0.4 | django-filter==22.1 | ||||||
|  | django-picklefield==3.1; python_version >= '3' | ||||||
|  | -e git+https://github.com/paperless-ngx/django-q.git@bf20d57f859a7d872d5979cd8879fac9c9df981c#egg=django-q | ||||||
| djangorestframework==3.13.1 | djangorestframework==3.13.1 | ||||||
| filelock==3.7.0 | filelock==3.7.1 | ||||||
| fuzzywuzzy[speedup]==0.18.0 | fuzzywuzzy[speedup]==0.18.0 | ||||||
| gunicorn==20.1.0 | gunicorn==20.1.0 | ||||||
| h11==0.13.0; python_version >= '3.6' | h11==0.13.0; python_version >= '3.6' | ||||||
| @@ -44,50 +39,50 @@ hiredis==2.0.0; python_version >= '3.6' | |||||||
| httptools==0.4.0 | httptools==0.4.0 | ||||||
| humanfriendly==10.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' | humanfriendly==10.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' | ||||||
| hyperlink==21.0.0 | hyperlink==21.0.0 | ||||||
| idna==3.3; python_version >= '3' | idna==3.3; python_version >= '3.5' | ||||||
| imap-tools==0.55.0 | imap-tools==0.56.0 | ||||||
| img2pdf==0.4.4 | img2pdf==0.4.4 | ||||||
| importlib-resources==5.7.1; python_version < '3.9' | importlib-resources==5.8.0; python_version < '3.9' | ||||||
| incremental==21.3.0 | incremental==21.3.0 | ||||||
| inotify-simple==1.3.5; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' | inotify-simple==1.3.5; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' | ||||||
| inotifyrecursive==0.3.5 | inotifyrecursive==0.3.5 | ||||||
| joblib==1.1.0; python_version >= '3.6' | joblib==1.1.0; python_version >= '3.6' | ||||||
| langdetect==1.0.9 | langdetect==1.0.9 | ||||||
| lxml==4.8.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' | lxml==4.9.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' | ||||||
| msgpack==1.0.3 | msgpack==1.0.4 | ||||||
| numpy==1.22.3; python_version >= '3.8' | numpy==1.23.1; python_version >= '3.8' | ||||||
| ocrmypdf==13.4.4 | ocrmypdf==13.6.0 | ||||||
| packaging==21.3; python_version >= '3.6' | packaging==21.3; python_version >= '3.6' | ||||||
| pathvalidate==2.5.0 | pathvalidate==2.5.0 | ||||||
| pdf2image==1.16.0 | pdf2image==1.16.0 | ||||||
| pdfminer.six==20220506 | pdfminer.six==20220524 | ||||||
| pikepdf==5.1.3 | pikepdf==5.3.1 | ||||||
| pillow==9.1.0 | pillow==9.2.0 | ||||||
| pluggy==1.0.0; python_version >= '3.6' | pluggy==1.0.0; python_version >= '3.6' | ||||||
| portalocker==2.4.0; python_version >= '3' | portalocker==2.5.1; python_version >= '3' | ||||||
| psycopg2==2.9.3 | psycopg2==2.9.3 | ||||||
| pyasn1-modules==0.2.8 |  | ||||||
| pyasn1==0.4.8 | pyasn1==0.4.8 | ||||||
|  | pyasn1-modules==0.2.8 | ||||||
| pycparser==2.21 | pycparser==2.21 | ||||||
| pyopenssl==22.0.0 | pyopenssl==22.0.0 | ||||||
| pyparsing==3.0.9; python_full_version >= '3.6.8' | pyparsing==3.0.9; python_full_version >= '3.6.8' | ||||||
| python-dateutil==2.8.2 | python-dateutil==2.8.2 | ||||||
| python-dotenv==0.20.0 | python-dotenv==0.20.0 | ||||||
| python-gnupg==0.4.8 | python-gnupg==0.4.9 | ||||||
| python-levenshtein==0.12.2 | python-levenshtein==0.12.2 | ||||||
| python-magic==0.4.25 | python-magic==0.4.27 | ||||||
| pytz-deprecation-shim==0.1.0.post0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' |  | ||||||
| pytz==2022.1 | pytz==2022.1 | ||||||
|  | pytz-deprecation-shim==0.1.0.post0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' | ||||||
| pyyaml==6.0 | pyyaml==6.0 | ||||||
| pyzbar==0.1.9 | pyzbar==0.1.9 | ||||||
| redis==3.5.3 | redis==4.3.4 | ||||||
| regex==2022.3.2; python_version >= '3.6' | regex==2022.3.2; python_version >= '3.6' | ||||||
| reportlab==3.6.9; python_version >= '3.7' and python_version < '4' | reportlab==3.6.11; python_version >= '3.7' and python_version < '4' | ||||||
| requests==2.27.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' | requests==2.28.1; python_version >= '3.7' and python_version < '4' | ||||||
| scikit-learn==1.0.2 | scikit-learn==1.1.1 | ||||||
| scipy==1.8.0; python_version < '3.11' and python_version >= '3.8' | scipy==1.8.1; python_version < '3.11' and python_version >= '3.8' | ||||||
| service-identity==21.1.0 | service-identity==21.1.0 | ||||||
| setuptools==62.2.0; python_version >= '3.7' | setuptools==63.1.0; python_version >= '3.7' | ||||||
| six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' | six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' | ||||||
| sniffio==1.2.0; python_version >= '3.5' | sniffio==1.2.0; python_version >= '3.5' | ||||||
| sqlparse==0.4.2; python_version >= '3.5' | sqlparse==0.4.2; python_version >= '3.5' | ||||||
| @@ -96,17 +91,18 @@ tika==1.24 | |||||||
| tqdm==4.64.0 | tqdm==4.64.0 | ||||||
| twisted[tls]==22.4.0; python_full_version >= '3.6.7' | twisted[tls]==22.4.0; python_full_version >= '3.6.7' | ||||||
| txaio==22.2.1; python_version >= '3.6' | txaio==22.2.1; python_version >= '3.6' | ||||||
| typing-extensions==4.2.0; python_version >= '3.7' | typing-extensions==4.3.0; python_version >= '3.7' | ||||||
| tzdata==2022.1; python_version >= '3.6' | tzdata==2022.1; python_version >= '3.6' | ||||||
| tzlocal==4.2; python_version >= '3.6' | tzlocal==4.2; python_version >= '3.6' | ||||||
| urllib3==1.26.9; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4' | urllib3==1.26.10; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4' | ||||||
| uvicorn[standard]==0.17.6 | uvicorn[standard]==0.18.2 | ||||||
| uvloop==0.16.0 | uvloop==0.16.0 | ||||||
| watchdog==2.1.8 | watchdog==2.1.9 | ||||||
| watchgod==0.8.2 | watchfiles==0.15.0 | ||||||
| wcwidth==0.2.5 | wcwidth==0.2.5 | ||||||
| websockets==10.3 | websockets==10.3 | ||||||
| whitenoise==6.0.0 | whitenoise==6.2.0 | ||||||
| whoosh==2.7.4 | whoosh==2.7.4 | ||||||
|  | wrapt==1.14.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' | ||||||
| zipp==3.8.0; python_version < '3.9' | zipp==3.8.0; python_version < '3.9' | ||||||
| zope.interface==5.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' | zope.interface==5.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								src-ui/cypress.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,13 @@ | |||||||
|  | import { defineConfig } from 'cypress' | ||||||
|  |  | ||||||
|  | export default defineConfig({ | ||||||
|  |   videosFolder: 'cypress/videos', | ||||||
|  |   screenshotsFolder: 'cypress/screenshots', | ||||||
|  |   fixturesFolder: 'cypress/fixtures', | ||||||
|  |   e2e: { | ||||||
|  |     setupNodeEvents(on, config) { | ||||||
|  |       return require('./cypress/plugins/index.ts')(on, config) | ||||||
|  |     }, | ||||||
|  |     baseUrl: 'http://localhost:4200', | ||||||
|  |   }, | ||||||
|  | }) | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| { |  | ||||||
|   "integrationFolder": "cypress/integration", |  | ||||||
|   "supportFile": "cypress/support/index.ts", |  | ||||||
|   "videosFolder": "cypress/videos", |  | ||||||
|   "screenshotsFolder": "cypress/screenshots", |  | ||||||
|   "pluginsFile": "cypress/plugins/index.ts", |  | ||||||
|   "fixturesFolder": "cypress/fixtures", |  | ||||||
|   "baseUrl": "http://localhost:4200" |  | ||||||
| } |  | ||||||
| @@ -1,10 +1,9 @@ | |||||||
| describe('document-detail', () => { | describe('document-detail', () => { | ||||||
|   beforeEach(() => { |   beforeEach(() => { | ||||||
|  |     // also uses global fixtures from cypress/support/e2e.ts
 | ||||||
|  | 
 | ||||||
|     this.modifiedDocuments = [] |     this.modifiedDocuments = [] | ||||||
| 
 | 
 | ||||||
|     cy.intercept('http://localhost:8000/api/ui_settings/', { |  | ||||||
|       fixture: 'ui_settings/settings.json', |  | ||||||
|     }) |  | ||||||
|     cy.fixture('documents/documents.json').then((documentsJson) => { |     cy.fixture('documents/documents.json').then((documentsJson) => { | ||||||
|       cy.intercept('GET', 'http://localhost:8000/api/documents/1/', (req) => { |       cy.intercept('GET', 'http://localhost:8000/api/documents/1/', (req) => { | ||||||
|         let response = { ...documentsJson } |         let response = { ...documentsJson } | ||||||
| @@ -18,30 +17,6 @@ describe('document-detail', () => { | |||||||
|       req.reply({ result: 'OK' }) |       req.reply({ result: 'OK' }) | ||||||
|     }).as('saveDoc') |     }).as('saveDoc') | ||||||
| 
 | 
 | ||||||
|     cy.intercept('http://localhost:8000/api/documents/1/metadata/', { |  | ||||||
|       fixture: 'documents/1/metadata.json', |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     cy.intercept('http://localhost:8000/api/documents/1/suggestions/', { |  | ||||||
|       fixture: 'documents/1/suggestions.json', |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     cy.intercept('http://localhost:8000/api/saved_views/*', { |  | ||||||
|       fixture: 'saved_views/savedviews.json', |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     cy.intercept('http://localhost:8000/api/tags/*', { |  | ||||||
|       fixture: 'tags/tags.json', |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     cy.intercept('http://localhost:8000/api/correspondents/*', { |  | ||||||
|       fixture: 'correspondents/correspondents.json', |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     cy.intercept('http://localhost:8000/api/document_types/*', { |  | ||||||
|       fixture: 'document_types/doctypes.json', |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     cy.viewport(1024, 1024) |     cy.viewport(1024, 1024) | ||||||
|     cy.visit('/documents/1/') |     cy.visit('/documents/1/') | ||||||
|   }) |   }) | ||||||
| @@ -1,11 +1,9 @@ | |||||||
| describe('documents-list', () => { | describe('documents-list', () => { | ||||||
|   beforeEach(() => { |   beforeEach(() => { | ||||||
|  |     // also uses global fixtures from cypress/support/e2e.ts
 | ||||||
|  | 
 | ||||||
|     this.bulkEdits = {} |     this.bulkEdits = {} | ||||||
| 
 | 
 | ||||||
|     // mock API methods
 |  | ||||||
|     cy.intercept('http://localhost:8000/api/ui_settings/', { |  | ||||||
|       fixture: 'ui_settings/settings.json', |  | ||||||
|     }) |  | ||||||
|     cy.fixture('documents/documents.json').then((documentsJson) => { |     cy.fixture('documents/documents.json').then((documentsJson) => { | ||||||
|       // bulk edit
 |       // bulk edit
 | ||||||
|       cy.intercept( |       cy.intercept( | ||||||
| @@ -56,40 +54,25 @@ describe('documents-list', () => { | |||||||
|       }) |       }) | ||||||
|     }) |     }) | ||||||
| 
 | 
 | ||||||
|     cy.intercept('http://localhost:8000/api/documents/1/thumb/', { |     cy.viewport(1280, 1024) | ||||||
|       fixture: 'documents/lorem-ipsum.png', |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     cy.intercept('http://localhost:8000/api/tags/*', { |  | ||||||
|       fixture: 'tags/tags.json', |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     cy.intercept('http://localhost:8000/api/correspondents/*', { |  | ||||||
|       fixture: 'correspondents/correspondents.json', |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     cy.intercept('http://localhost:8000/api/document_types/*', { |  | ||||||
|       fixture: 'document_types/doctypes.json', |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     cy.visit('/documents') |     cy.visit('/documents') | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|   it('should show a list of documents rendered as cards with thumbnails', () => { |   it('should show a list of documents rendered as cards with thumbnails', () => { | ||||||
|     cy.contains('3 documents') |     cy.contains('3 documents') | ||||||
|     cy.contains('lorem-ipsum') |     cy.contains('lorem ipsum') | ||||||
|     cy.get('app-document-card-small:first-of-type img') |     cy.get('app-document-card-small:first-of-type img') | ||||||
|       .invoke('attr', 'src') |       .invoke('attr', 'src') | ||||||
|       .should('eq', 'http://localhost:8000/api/documents/1/thumb/') |       .should('eq', 'http://localhost:8000/api/documents/1/thumb/') | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|   it('should change to table "details" view', () => { |   it('should change to table "details" view', () => { | ||||||
|     cy.get('div.btn-group-toggle input[value="details"]').parent().click() |     cy.get('div.btn-group input[value="details"]').next().click() | ||||||
|     cy.get('table') |     cy.get('table') | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|   it('should change to large cards view', () => { |   it('should change to large cards view', () => { | ||||||
|     cy.get('div.btn-group-toggle input[value="largeCards"]').parent().click() |     cy.get('div.btn-group input[value="largeCards"]').next().click() | ||||||
|     cy.get('app-document-card-large') |     cy.get('app-document-card-large') | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
							
								
								
									
										331
									
								
								src-ui/cypress/e2e/documents/query-params.cy.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,331 @@ | |||||||
|  | import { PaperlessDocument } from 'src/app/data/paperless-document' | ||||||
|  |  | ||||||
|  | describe('documents query params', () => { | ||||||
|  |   beforeEach(() => { | ||||||
|  |     // also uses global fixtures from cypress/support/e2e.ts | ||||||
|  |  | ||||||
|  |     cy.fixture('documents/documents.json').then((documentsJson) => { | ||||||
|  |       // mock api filtering | ||||||
|  |       cy.intercept('GET', 'http://localhost:8000/api/documents/*', (req) => { | ||||||
|  |         let response = { ...documentsJson } | ||||||
|  |  | ||||||
|  |         if (req.query.hasOwnProperty('ordering')) { | ||||||
|  |           const sort_field = req.query['ordering'].toString().replace('-', '') | ||||||
|  |           const reverse = req.query['ordering'].toString().indexOf('-') !== -1 | ||||||
|  |           response.results = ( | ||||||
|  |             documentsJson.results as Array<PaperlessDocument> | ||||||
|  |           ).sort((docA, docB) => { | ||||||
|  |             let result = 0 | ||||||
|  |             switch (sort_field) { | ||||||
|  |               case 'created': | ||||||
|  |               case 'added': | ||||||
|  |                 result = | ||||||
|  |                   new Date(docA[sort_field]) < new Date(docB[sort_field]) | ||||||
|  |                     ? -1 | ||||||
|  |                     : 1 | ||||||
|  |                 break | ||||||
|  |               case 'archive_serial_number': | ||||||
|  |                 result = docA[sort_field] < docB[sort_field] ? -1 : 1 | ||||||
|  |                 break | ||||||
|  |             } | ||||||
|  |             if (reverse) result = -result | ||||||
|  |             return result | ||||||
|  |           }) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (req.query.hasOwnProperty('tags__id__in')) { | ||||||
|  |           const tag_ids: Array<number> = req.query['tags__id__in'] | ||||||
|  |             .toString() | ||||||
|  |             .split(',') | ||||||
|  |             .map((v) => +v) | ||||||
|  |           response.results = ( | ||||||
|  |             documentsJson.results as Array<PaperlessDocument> | ||||||
|  |           ).filter( | ||||||
|  |             (d) => | ||||||
|  |               d.tags.length > 0 && | ||||||
|  |               d.tags.filter((t) => tag_ids.includes(t)).length > 0 | ||||||
|  |           ) | ||||||
|  |           response.count = response.results.length | ||||||
|  |         } else if (req.query.hasOwnProperty('tags__id__none')) { | ||||||
|  |           const tag_ids: Array<number> = req.query['tags__id__none'] | ||||||
|  |             .toString() | ||||||
|  |             .split(',') | ||||||
|  |             .map((v) => +v) | ||||||
|  |           response.results = ( | ||||||
|  |             documentsJson.results as Array<PaperlessDocument> | ||||||
|  |           ).filter((d) => d.tags.filter((t) => tag_ids.includes(t)).length == 0) | ||||||
|  |           response.count = response.results.length | ||||||
|  |         } else if ( | ||||||
|  |           req.query.hasOwnProperty('is_tagged') && | ||||||
|  |           req.query['is_tagged'] == '0' | ||||||
|  |         ) { | ||||||
|  |           response.results = ( | ||||||
|  |             documentsJson.results as Array<PaperlessDocument> | ||||||
|  |           ).filter((d) => d.tags.length == 0) | ||||||
|  |           response.count = response.results.length | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (req.query.hasOwnProperty('document_type__id')) { | ||||||
|  |           const doctype_id = +req.query['document_type__id'] | ||||||
|  |           response.results = ( | ||||||
|  |             documentsJson.results as Array<PaperlessDocument> | ||||||
|  |           ).filter((d) => d.document_type == doctype_id) | ||||||
|  |           response.count = response.results.length | ||||||
|  |         } else if ( | ||||||
|  |           req.query.hasOwnProperty('document_type__isnull') && | ||||||
|  |           req.query['document_type__isnull'] == '1' | ||||||
|  |         ) { | ||||||
|  |           response.results = ( | ||||||
|  |             documentsJson.results as Array<PaperlessDocument> | ||||||
|  |           ).filter((d) => d.document_type == undefined) | ||||||
|  |           response.count = response.results.length | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (req.query.hasOwnProperty('correspondent__id')) { | ||||||
|  |           const correspondent_id = +req.query['correspondent__id'] | ||||||
|  |           response.results = ( | ||||||
|  |             documentsJson.results as Array<PaperlessDocument> | ||||||
|  |           ).filter((d) => d.correspondent == correspondent_id) | ||||||
|  |           response.count = response.results.length | ||||||
|  |         } else if ( | ||||||
|  |           req.query.hasOwnProperty('correspondent__isnull') && | ||||||
|  |           req.query['correspondent__isnull'] == '1' | ||||||
|  |         ) { | ||||||
|  |           response.results = ( | ||||||
|  |             documentsJson.results as Array<PaperlessDocument> | ||||||
|  |           ).filter((d) => d.correspondent == undefined) | ||||||
|  |           response.count = response.results.length | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (req.query.hasOwnProperty('storage_path__id')) { | ||||||
|  |           const storage_path_id = +req.query['storage_path__id'] | ||||||
|  |           response.results = ( | ||||||
|  |             documentsJson.results as Array<PaperlessDocument> | ||||||
|  |           ).filter((d) => d.storage_path == storage_path_id) | ||||||
|  |           response.count = response.results.length | ||||||
|  |         } else if ( | ||||||
|  |           req.query.hasOwnProperty('storage_path__isnull') && | ||||||
|  |           req.query['storage_path__isnull'] == '1' | ||||||
|  |         ) { | ||||||
|  |           response.results = ( | ||||||
|  |             documentsJson.results as Array<PaperlessDocument> | ||||||
|  |           ).filter((d) => d.storage_path == undefined) | ||||||
|  |           response.count = response.results.length | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (req.query.hasOwnProperty('created__date__gt')) { | ||||||
|  |           const date = new Date(req.query['created__date__gt']) | ||||||
|  |           response.results = ( | ||||||
|  |             documentsJson.results as Array<PaperlessDocument> | ||||||
|  |           ).filter((d) => new Date(d.created) > date) | ||||||
|  |           response.count = response.results.length | ||||||
|  |         } else if (req.query.hasOwnProperty('created__date__lt')) { | ||||||
|  |           const date = new Date(req.query['created__date__lt']) | ||||||
|  |           response.results = ( | ||||||
|  |             documentsJson.results as Array<PaperlessDocument> | ||||||
|  |           ).filter((d) => new Date(d.created) < date) | ||||||
|  |           response.count = response.results.length | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (req.query.hasOwnProperty('added__date__gt')) { | ||||||
|  |           const date = new Date(req.query['added__date__gt']) | ||||||
|  |           response.results = ( | ||||||
|  |             documentsJson.results as Array<PaperlessDocument> | ||||||
|  |           ).filter((d) => new Date(d.added) > date) | ||||||
|  |           response.count = response.results.length | ||||||
|  |         } else if (req.query.hasOwnProperty('added__date__lt')) { | ||||||
|  |           const date = new Date(req.query['added__date__lt']) | ||||||
|  |           response.results = ( | ||||||
|  |             documentsJson.results as Array<PaperlessDocument> | ||||||
|  |           ).filter((d) => new Date(d.added) < date) | ||||||
|  |           response.count = response.results.length | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (req.query.hasOwnProperty('title_content')) { | ||||||
|  |           const title_content_regexp = new RegExp( | ||||||
|  |             req.query['title_content'].toString(), | ||||||
|  |             'i' | ||||||
|  |           ) | ||||||
|  |           response.results = ( | ||||||
|  |             documentsJson.results as Array<PaperlessDocument> | ||||||
|  |           ).filter( | ||||||
|  |             (d) => | ||||||
|  |               title_content_regexp.test(d.title) || | ||||||
|  |               title_content_regexp.test(d.content) | ||||||
|  |           ) | ||||||
|  |           response.count = response.results.length | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (req.query.hasOwnProperty('archive_serial_number')) { | ||||||
|  |           const asn = +req.query['archive_serial_number'] | ||||||
|  |           response.results = ( | ||||||
|  |             documentsJson.results as Array<PaperlessDocument> | ||||||
|  |           ).filter((d) => d.archive_serial_number == asn) | ||||||
|  |           response.count = response.results.length | ||||||
|  |         } else if (req.query.hasOwnProperty('archive_serial_number__isnull')) { | ||||||
|  |           const isnull = req.query['storage_path__isnull'] == '1' | ||||||
|  |           response.results = ( | ||||||
|  |             documentsJson.results as Array<PaperlessDocument> | ||||||
|  |           ).filter((d) => | ||||||
|  |             isnull | ||||||
|  |               ? d.archive_serial_number == undefined | ||||||
|  |               : d.archive_serial_number != undefined | ||||||
|  |           ) | ||||||
|  |           response.count = response.results.length | ||||||
|  |         } else if (req.query.hasOwnProperty('archive_serial_number__gt')) { | ||||||
|  |           const asn = +req.query['archive_serial_number__gt'] | ||||||
|  |           response.results = ( | ||||||
|  |             documentsJson.results as Array<PaperlessDocument> | ||||||
|  |           ).filter( | ||||||
|  |             (d) => d.archive_serial_number > 0 && d.archive_serial_number > asn | ||||||
|  |           ) | ||||||
|  |           response.count = response.results.length | ||||||
|  |         } else if (req.query.hasOwnProperty('archive_serial_number__lt')) { | ||||||
|  |           const asn = +req.query['archive_serial_number__lt'] | ||||||
|  |           response.results = ( | ||||||
|  |             documentsJson.results as Array<PaperlessDocument> | ||||||
|  |           ).filter( | ||||||
|  |             (d) => d.archive_serial_number > 0 && d.archive_serial_number < asn | ||||||
|  |           ) | ||||||
|  |           response.count = response.results.length | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         req.reply(response) | ||||||
|  |       }) | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of documents sorted by created', () => { | ||||||
|  |     cy.visit('/documents?sort=created') | ||||||
|  |     cy.get('app-document-card-small').first().contains('No latin title') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of documents reverse sorted by created', () => { | ||||||
|  |     cy.visit('/documents?sort=created&reverse=true') | ||||||
|  |     cy.get('app-document-card-small').first().contains('sit amet') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of documents sorted by added', () => { | ||||||
|  |     cy.visit('/documents?sort=added') | ||||||
|  |     cy.get('app-document-card-small').first().contains('No latin title') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of documents reverse sorted by added', () => { | ||||||
|  |     cy.visit('/documents?sort=added&reverse=true') | ||||||
|  |     cy.get('app-document-card-small').first().contains('sit amet') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of documents filtered by any tags', () => { | ||||||
|  |     cy.visit('/documents?sort=created&reverse=true&tags__id__in=2,4,5') | ||||||
|  |     cy.contains('3 documents') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of documents filtered by excluded tags', () => { | ||||||
|  |     cy.visit('/documents?sort=created&reverse=true&tags__id__none=2,4') | ||||||
|  |     cy.contains('One document') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of documents filtered by no tags', () => { | ||||||
|  |     cy.visit('/documents?sort=created&reverse=true&is_tagged=0') | ||||||
|  |     cy.contains('One document') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of documents filtered by document type', () => { | ||||||
|  |     cy.visit('/documents?sort=created&reverse=true&document_type__id=1') | ||||||
|  |     cy.contains('3 documents') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of documents filtered by no document type', () => { | ||||||
|  |     cy.visit('/documents?sort=created&reverse=true&document_type__isnull=1') | ||||||
|  |     cy.contains('One document') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of documents filtered by correspondent', () => { | ||||||
|  |     cy.visit('/documents?sort=created&reverse=true&correspondent__id=9') | ||||||
|  |     cy.contains('2 documents') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of documents filtered by no correspondent', () => { | ||||||
|  |     cy.visit('/documents?sort=created&reverse=true&correspondent__isnull=1') | ||||||
|  |     cy.contains('2 documents') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of documents filtered by storage path', () => { | ||||||
|  |     cy.visit('/documents?sort=created&reverse=true&storage_path__id=2') | ||||||
|  |     cy.contains('One document') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of documents filtered by no storage path', () => { | ||||||
|  |     cy.visit('/documents?sort=created&reverse=true&storage_path__isnull=1') | ||||||
|  |     cy.contains('3 documents') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of documents filtered by title or content', () => { | ||||||
|  |     cy.visit('/documents?sort=created&reverse=true&title_content=lorem') | ||||||
|  |     cy.contains('2 documents') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of documents filtered by asn', () => { | ||||||
|  |     cy.visit('/documents?sort=created&reverse=true&archive_serial_number=12345') | ||||||
|  |     cy.contains('One document') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of documents filtered by empty asn', () => { | ||||||
|  |     cy.visit( | ||||||
|  |       '/documents?sort=created&reverse=true&archive_serial_number__isnull=1' | ||||||
|  |     ) | ||||||
|  |     cy.contains('2 documents') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of documents filtered by non-empty asn', () => { | ||||||
|  |     cy.visit( | ||||||
|  |       '/documents?sort=created&reverse=true&archive_serial_number__isnull=0' | ||||||
|  |     ) | ||||||
|  |     cy.contains('2 documents') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of documents filtered by asn greater than', () => { | ||||||
|  |     cy.visit( | ||||||
|  |       '/documents?sort=created&reverse=true&archive_serial_number__gt=12346' | ||||||
|  |     ) | ||||||
|  |     cy.contains('One document') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of documents filtered by asn less than', () => { | ||||||
|  |     cy.visit( | ||||||
|  |       '/documents?sort=created&reverse=true&archive_serial_number__lt=12346' | ||||||
|  |     ) | ||||||
|  |     cy.contains('One document') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of documents filtered by created date greater than', () => { | ||||||
|  |     cy.visit( | ||||||
|  |       '/documents?sort=created&reverse=true&created__date__gt=2022-03-23' | ||||||
|  |     ) | ||||||
|  |     cy.contains('3 documents') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of documents filtered by created date less than', () => { | ||||||
|  |     cy.visit( | ||||||
|  |       '/documents?sort=created&reverse=true&created__date__lt=2022-03-23' | ||||||
|  |     ) | ||||||
|  |     cy.contains('One document') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of documents filtered by added date greater than', () => { | ||||||
|  |     cy.visit('/documents?sort=created&reverse=true&added__date__gt=2022-03-24') | ||||||
|  |     cy.contains('2 documents') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of documents filtered by added date less than', () => { | ||||||
|  |     cy.visit('/documents?sort=created&reverse=true&added__date__lt=2022-03-24') | ||||||
|  |     cy.contains('2 documents') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of documents filtered by multiple filters', () => { | ||||||
|  |     cy.visit( | ||||||
|  |       '/documents?sort=created&reverse=true&document_type__id=1&correspondent__id=9&tags__id__in=4,5' | ||||||
|  |     ) | ||||||
|  |     cy.contains('2 documents') | ||||||
|  |   }) | ||||||
|  | }) | ||||||
| @@ -1,15 +1,5 @@ | |||||||
| describe('manage', () => { | describe('manage', () => { | ||||||
|   beforeEach(() => { |   // also uses global fixtures from cypress/support/e2e.ts
 | ||||||
|     cy.intercept('http://localhost:8000/api/ui_settings/', { |  | ||||||
|       fixture: 'ui_settings/settings.json', |  | ||||||
|     }) |  | ||||||
|     cy.intercept('http://localhost:8000/api/correspondents/*', { |  | ||||||
|       fixture: 'correspondents/correspondents.json', |  | ||||||
|     }) |  | ||||||
|     cy.intercept('http://localhost:8000/api/tags/*', { |  | ||||||
|       fixture: 'tags/tags.json', |  | ||||||
|     }) |  | ||||||
|   }) |  | ||||||
| 
 | 
 | ||||||
|   it('should show a list of correspondents with bottom pagination as well', () => { |   it('should show a list of correspondents with bottom pagination as well', () => { | ||||||
|     cy.visit('/correspondents') |     cy.visit('/correspondents') | ||||||
| @@ -1,5 +1,7 @@ | |||||||
| describe('settings', () => { | describe('settings', () => { | ||||||
|   beforeEach(() => { |   beforeEach(() => { | ||||||
|  |     // also uses global fixtures from cypress/support/e2e.ts
 | ||||||
|  | 
 | ||||||
|     this.modifiedViews = [] |     this.modifiedViews = [] | ||||||
| 
 | 
 | ||||||
|     // mock API methods
 |     // mock API methods
 | ||||||
| @@ -42,14 +44,6 @@ describe('settings', () => { | |||||||
|           req.reply(response) |           req.reply(response) | ||||||
|         }) |         }) | ||||||
|       }) |       }) | ||||||
| 
 |  | ||||||
|       cy.intercept('http://localhost:8000/api/documents/1/metadata/', { |  | ||||||
|         fixture: 'documents/1/metadata.json', |  | ||||||
|       }) |  | ||||||
| 
 |  | ||||||
|       cy.intercept('http://localhost:8000/api/documents/1/suggestions/', { |  | ||||||
|         fixture: 'documents/1/suggestions.json', |  | ||||||
|       }) |  | ||||||
|     }) |     }) | ||||||
| 
 | 
 | ||||||
|     cy.viewport(1024, 1024) |     cy.viewport(1024, 1024) | ||||||
							
								
								
									
										60
									
								
								src-ui/cypress/e2e/tasks/tasks.cy.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,60 @@ | |||||||
|  | describe('tasks', () => { | ||||||
|  |   beforeEach(() => { | ||||||
|  |     this.dismissedTasks = new Set<number>() | ||||||
|  |  | ||||||
|  |     cy.fixture('tasks/tasks.json').then((tasksViewsJson) => { | ||||||
|  |       // acknowledge tasks POST | ||||||
|  |       cy.intercept( | ||||||
|  |         'POST', | ||||||
|  |         'http://localhost:8000/api/acknowledge_tasks/', | ||||||
|  |         (req) => { | ||||||
|  |           req.body['tasks'].forEach((t) => this.dismissedTasks.add(t)) // store this for later | ||||||
|  |           req.reply({ result: 'OK' }) | ||||||
|  |         } | ||||||
|  |       ) | ||||||
|  |  | ||||||
|  |       cy.intercept('GET', 'http://localhost:8000/api/tasks/', (req) => { | ||||||
|  |         let response = [...tasksViewsJson] | ||||||
|  |         if (this.dismissedTasks.size) { | ||||||
|  |           response = response.filter((t) => { | ||||||
|  |             return !this.dismissedTasks.has(t.id) | ||||||
|  |           }) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         req.reply(response) | ||||||
|  |       }).as('tasks') | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     cy.visit('/tasks') | ||||||
|  |     cy.wait('@tasks') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show a list of dismissable tasks in tabs', () => { | ||||||
|  |     cy.get('tbody').find('tr:visible').its('length').should('eq', 10) // double because collapsible result tr | ||||||
|  |     cy.wait(500) // stabilizes the test, for some reason... | ||||||
|  |     cy.get('tbody') | ||||||
|  |       .find('button:visible') | ||||||
|  |       .contains('Dismiss') | ||||||
|  |       .first() | ||||||
|  |       .click() | ||||||
|  |       .wait('@tasks') | ||||||
|  |       .wait(2000) | ||||||
|  |       .then(() => { | ||||||
|  |         cy.get('tbody').find('tr:visible').its('length').should('eq', 8) // double because collapsible result tr | ||||||
|  |       }) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should allow toggling all tasks in list and warn on dismiss', () => { | ||||||
|  |     cy.get('thead').find('input[type="checkbox"]').first().click() | ||||||
|  |     cy.get('body').find('button').contains('Dismiss selected').first().click() | ||||||
|  |     cy.contains('Confirm') | ||||||
|  |     cy.get('.modal') | ||||||
|  |       .contains('button', 'Dismiss') | ||||||
|  |       .click() | ||||||
|  |       .wait('@tasks') | ||||||
|  |       .wait(2000) | ||||||
|  |       .then(() => { | ||||||
|  |         cy.get('tbody').find('tr:visible').should('not.exist') | ||||||
|  |       }) | ||||||
|  |   }) | ||||||
|  | }) | ||||||
| @@ -0,0 +1 @@ | |||||||
|  | {"version":"v1.7.1","update_available":false,"feature_is_set":true} | ||||||
							
								
								
									
										17
									
								
								src-ui/cypress/fixtures/storage_paths/storage_paths.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,17 @@ | |||||||
|  | { | ||||||
|  |     "count": 1, | ||||||
|  |     "next": null, | ||||||
|  |     "previous": null, | ||||||
|  |     "results": [ | ||||||
|  |         { | ||||||
|  |             "id": 2, | ||||||
|  |             "slug": "year-title", | ||||||
|  |             "name": "Year - Title", | ||||||
|  |             "path": "{created_year}/{title}", | ||||||
|  |             "match": "", | ||||||
|  |             "matching_algorithm": 6, | ||||||
|  |             "is_insensitive": true, | ||||||
|  |             "document_count": 1 | ||||||
|  |         } | ||||||
|  |     ] | ||||||
|  | } | ||||||
| @@ -1 +1,103 @@ | |||||||
| {"count":8,"next":null,"previous":null,"results":[{"id":4,"slug":"another-sample-tag","name":"Another Sample Tag","color":"#a6cee3","text_color":"#000000","match":"","matching_algorithm":6,"is_insensitive":true,"is_inbox_tag":false,"document_count":3},{"id":7,"slug":"newone","name":"NewOne","color":"#9e4ad1","text_color":"#ffffff","match":"","matching_algorithm":1,"is_insensitive":true,"is_inbox_tag":false,"document_count":2},{"id":6,"slug":"partial-tag","name":"Partial Tag","color":"#72dba7","text_color":"#000000","match":"","matching_algorithm":1,"is_insensitive":true,"is_inbox_tag":false,"document_count":1},{"id":2,"slug":"tag-2","name":"Tag 2","color":"#612db7","text_color":"#ffffff","match":"","matching_algorithm":1,"is_insensitive":true,"is_inbox_tag":false,"document_count":3},{"id":3,"slug":"tag-3","name":"Tag 3","color":"#b2df8a","text_color":"#000000","match":"","matching_algorithm":1,"is_insensitive":true,"is_inbox_tag":false,"document_count":4},{"id":5,"slug":"tagwithpartial","name":"TagWithPartial","color":"#3b2db4","text_color":"#ffffff","match":"","matching_algorithm":6,"is_insensitive":true,"is_inbox_tag":false,"document_count":2},{"id":8,"slug":"test-another","name":"Test Another","color":"#3ccea5","text_color":"#000000","match":"","matching_algorithm":4,"is_insensitive":true,"is_inbox_tag":false,"document_count":0},{"id":1,"slug":"test-tag","name":"Test Tag","color":"#fb9a99","text_color":"#000000","match":"","matching_algorithm":1,"is_insensitive":true,"is_inbox_tag":false,"document_count":4}]} | { | ||||||
|  |     "count": 8, | ||||||
|  |     "next": null, | ||||||
|  |     "previous": null, | ||||||
|  |     "results": [ | ||||||
|  |         { | ||||||
|  |             "id": 4, | ||||||
|  |             "slug": "another-sample-tag", | ||||||
|  |             "name": "Another Sample Tag", | ||||||
|  |             "color": "#a6cee3", | ||||||
|  |             "text_color": "#000000", | ||||||
|  |             "match": "", | ||||||
|  |             "matching_algorithm": 6, | ||||||
|  |             "is_insensitive": true, | ||||||
|  |             "is_inbox_tag": false, | ||||||
|  |             "document_count": 3 | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             "id": 7, | ||||||
|  |             "slug": "newone", | ||||||
|  |             "name": "NewOne", | ||||||
|  |             "color": "#9e4ad1", | ||||||
|  |             "text_color": "#ffffff", | ||||||
|  |             "match": "", | ||||||
|  |             "matching_algorithm": 1, | ||||||
|  |             "is_insensitive": true, | ||||||
|  |             "is_inbox_tag": false, | ||||||
|  |             "document_count": 2 | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             "id": 6, | ||||||
|  |             "slug": "partial-tag", | ||||||
|  |             "name": "Partial Tag", | ||||||
|  |             "color": "#72dba7", | ||||||
|  |             "text_color": "#000000", | ||||||
|  |             "match": "", | ||||||
|  |             "matching_algorithm": 1, | ||||||
|  |             "is_insensitive": true, | ||||||
|  |             "is_inbox_tag": false, | ||||||
|  |             "document_count": 1 | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             "id": 2, | ||||||
|  |             "slug": "tag-2", | ||||||
|  |             "name": "Tag 2", | ||||||
|  |             "color": "#612db7", | ||||||
|  |             "text_color": "#ffffff", | ||||||
|  |             "match": "", | ||||||
|  |             "matching_algorithm": 1, | ||||||
|  |             "is_insensitive": true, | ||||||
|  |             "is_inbox_tag": false, | ||||||
|  |             "document_count": 3 | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             "id": 3, | ||||||
|  |             "slug": "tag-3", | ||||||
|  |             "name": "Tag 3", | ||||||
|  |             "color": "#b2df8a", | ||||||
|  |             "text_color": "#000000", | ||||||
|  |             "match": "", | ||||||
|  |             "matching_algorithm": 1, | ||||||
|  |             "is_insensitive": true, | ||||||
|  |             "is_inbox_tag": false, | ||||||
|  |             "document_count": 4 | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             "id": 5, | ||||||
|  |             "slug": "tagwithpartial", | ||||||
|  |             "name": "TagWithPartial", | ||||||
|  |             "color": "#3b2db4", | ||||||
|  |             "text_color": "#ffffff", | ||||||
|  |             "match": "", | ||||||
|  |             "matching_algorithm": 6, | ||||||
|  |             "is_insensitive": true, | ||||||
|  |             "is_inbox_tag": false, | ||||||
|  |             "document_count": 2 | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             "id": 8, | ||||||
|  |             "slug": "test-another", | ||||||
|  |             "name": "Test Another", | ||||||
|  |             "color": "#3ccea5", | ||||||
|  |             "text_color": "#000000", | ||||||
|  |             "match": "", | ||||||
|  |             "matching_algorithm": 4, | ||||||
|  |             "is_insensitive": true, | ||||||
|  |             "is_inbox_tag": false, | ||||||
|  |             "document_count": 0 | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             "id": 1, | ||||||
|  |             "slug": "test-tag", | ||||||
|  |             "name": "Test Tag", | ||||||
|  |             "color": "#fb9a99", | ||||||
|  |             "text_color": "#000000", | ||||||
|  |             "match": "", | ||||||
|  |             "matching_algorithm": 1, | ||||||
|  |             "is_insensitive": true, | ||||||
|  |             "is_inbox_tag": false, | ||||||
|  |             "document_count": 4 | ||||||
|  |         } | ||||||
|  |     ] | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								src-ui/cypress/fixtures/tasks/tasks.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										43
									
								
								src-ui/cypress/support/e2e.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,43 @@ | |||||||
|  | // mock API methods | ||||||
|  |  | ||||||
|  | beforeEach(() => { | ||||||
|  |   cy.intercept('http://localhost:8000/api/ui_settings/', { | ||||||
|  |     fixture: 'ui_settings/settings.json', | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   cy.intercept('http://localhost:8000/api/remote_version/', { | ||||||
|  |     fixture: 'remote_version/remote_version.json', | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   cy.intercept('http://localhost:8000/api/saved_views/*', { | ||||||
|  |     fixture: 'saved_views/savedviews.json', | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   cy.intercept('http://localhost:8000/api/tags/*', { | ||||||
|  |     fixture: 'tags/tags.json', | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   cy.intercept('http://localhost:8000/api/correspondents/*', { | ||||||
|  |     fixture: 'correspondents/correspondents.json', | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   cy.intercept('http://localhost:8000/api/document_types/*', { | ||||||
|  |     fixture: 'document_types/doctypes.json', | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   cy.intercept('http://localhost:8000/api/storage_paths/*', { | ||||||
|  |     fixture: 'storage_paths/storage_paths.json', | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   cy.intercept('http://localhost:8000/api/documents/1/metadata/', { | ||||||
|  |     fixture: 'documents/1/metadata.json', | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   cy.intercept('http://localhost:8000/api/documents/1/suggestions/', { | ||||||
|  |     fixture: 'documents/1/suggestions.json', | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   cy.intercept('http://localhost:8000/api/documents/1/thumb/', { | ||||||
|  |     fixture: 'documents/lorem-ipsum.png', | ||||||
|  |   }) | ||||||
|  | }) | ||||||
| @@ -1,17 +0,0 @@ | |||||||
| // *********************************************************** |  | ||||||
| // This example support/index.js is processed and |  | ||||||
| // loaded automatically before your test files. |  | ||||||
| // |  | ||||||
| // This is a great place to put global configuration and |  | ||||||
| // behavior that modifies Cypress. |  | ||||||
| // |  | ||||||
| // You can change the location of this file or turn off |  | ||||||
| // automatically serving support files with the |  | ||||||
| // 'supportFile' configuration option. |  | ||||||
| // |  | ||||||
| // You can read more here: |  | ||||||
| // https://on.cypress.io/configuration |  | ||||||
| // *********************************************************** |  | ||||||
|  |  | ||||||
| // When a command from ./commands is ready to use, import with `import './commands'` syntax |  | ||||||
| // import './commands'; |  | ||||||
							
								
								
									
										17345
									
								
								src-ui/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						| @@ -13,48 +13,48 @@ | |||||||
|   }, |   }, | ||||||
|   "private": true, |   "private": true, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@angular/common": "~13.3.5", |     "@angular/common": "~14.0.4", | ||||||
|     "@angular/compiler": "~13.3.5", |     "@angular/compiler": "~14.0.4", | ||||||
|     "@angular/core": "~13.3.5", |     "@angular/core": "~14.0.4", | ||||||
|     "@angular/forms": "~13.3.5", |     "@angular/forms": "~14.0.4", | ||||||
|     "@angular/localize": "~13.3.5", |     "@angular/localize": "~14.0.4", | ||||||
|     "@angular/platform-browser": "~13.3.5", |     "@angular/platform-browser": "~14.0.4", | ||||||
|     "@angular/platform-browser-dynamic": "~13.3.5", |     "@angular/platform-browser-dynamic": "~14.0.4", | ||||||
|     "@angular/router": "~13.3.5", |     "@angular/router": "~14.0.4", | ||||||
|     "@ng-bootstrap/ng-bootstrap": "^12.1.1", |     "@ng-bootstrap/ng-bootstrap": "^13.0.0-beta.1", | ||||||
|     "@ng-select/ng-select": "^8.1.1", |     "@ng-select/ng-select": "^9.0.2", | ||||||
|     "@ngneat/dirty-check-forms": "^3.0.2", |     "@ngneat/dirty-check-forms": "^3.0.2", | ||||||
|     "@popperjs/core": "^2.11.4", |     "@popperjs/core": "^2.11.5", | ||||||
|     "bootstrap": "^5.1.3", |     "bootstrap": "^5.1.3", | ||||||
|     "file-saver": "^2.0.5", |     "file-saver": "^2.0.5", | ||||||
|     "ng2-pdf-viewer": "^9.0.0", |     "ng2-pdf-viewer": "^9.0.0", | ||||||
|     "ngx-color": "^7.3.3", |     "ngx-color": "^7.3.3", | ||||||
|     "ngx-cookie-service": "^13.1.2", |     "ngx-cookie-service": "^14.0.1", | ||||||
|     "ngx-file-drop": "^13.0.0", |     "ngx-file-drop": "^13.0.0", | ||||||
|     "rxjs": "~7.5.5", |     "rxjs": "~7.5.5", | ||||||
|     "tslib": "^2.3.1", |     "tslib": "^2.3.1", | ||||||
|     "uuid": "^8.3.1", |     "uuid": "^8.3.1", | ||||||
|     "zone.js": "~0.11.4" |     "zone.js": "~0.11.6" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@angular-builders/jest": "13.0.3", |     "@angular-builders/jest": "14.0.0", | ||||||
|     "@angular-devkit/build-angular": "~13.3.4", |     "@angular-devkit/build-angular": "~14.0.4", | ||||||
|     "@angular/cli": "~13.3.4", |     "@angular/cli": "~14.0.4", | ||||||
|     "@angular/compiler-cli": "~13.3.5", |     "@angular/compiler-cli": "~14.0.4", | ||||||
|     "@types/jest": "27.4.1", |     "@types/jest": "28.1.4", | ||||||
|     "@types/node": "^17.0.30", |     "@types/node": "^18.0.0", | ||||||
|     "codelyzer": "^6.0.2", |     "codelyzer": "^6.0.2", | ||||||
|     "concurrently": "7.1.0", |     "concurrently": "7.2.2", | ||||||
|     "jest": "28.0.3", |     "jest": "28.1.2", | ||||||
|     "jest-environment-jsdom": "^28.0.2", |     "jest-environment-jsdom": "^28.1.2", | ||||||
|     "jest-preset-angular": "^12.0.0-next.1", |     "jest-preset-angular": "^12.1.0", | ||||||
|     "ts-node": "~10.7.0", |     "ts-node": "~10.8.1", | ||||||
|     "tslint": "~6.1.3", |     "tslint": "~6.1.3", | ||||||
|     "typescript": "~4.6.3", |     "typescript": "~4.6.3", | ||||||
|     "wait-on": "~6.0.1" |     "wait-on": "~6.0.1" | ||||||
|   }, |   }, | ||||||
|   "optionalDependencies": { |   "optionalDependencies": { | ||||||
|     "@cypress/schematic": "^1.6.0", |     "@cypress/schematic": "^2.0.0", | ||||||
|     "cypress": "~9.6.0" |     "cypress": "~10.3.0" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ import { NotFoundComponent } from './components/not-found/not-found.component' | |||||||
| import { DocumentAsnComponent } from './components/document-asn/document-asn.component' | import { DocumentAsnComponent } from './components/document-asn/document-asn.component' | ||||||
| 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' | ||||||
|  |  | ||||||
| const routes: Routes = [ | const routes: Routes = [ | ||||||
|   { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, |   { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, | ||||||
| @@ -35,6 +36,7 @@ const routes: Routes = [ | |||||||
|         component: SettingsComponent, |         component: SettingsComponent, | ||||||
|         canDeactivate: [DirtyFormGuard], |         canDeactivate: [DirtyFormGuard], | ||||||
|       }, |       }, | ||||||
|  |       { path: 'tasks', component: TasksComponent }, | ||||||
|     ], |     ], | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ import { ConsumerStatusService } from './services/consumer-status.service' | |||||||
| import { ToastService } from './services/toast.service' | import { ToastService } from './services/toast.service' | ||||||
| import { NgxFileDropEntry } from 'ngx-file-drop' | 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' | ||||||
|  |  | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-root', |   selector: 'app-root', | ||||||
| @@ -27,7 +28,8 @@ export class AppComponent implements OnInit, OnDestroy { | |||||||
|     private consumerStatusService: ConsumerStatusService, |     private consumerStatusService: ConsumerStatusService, | ||||||
|     private toastService: ToastService, |     private toastService: ToastService, | ||||||
|     private router: Router, |     private router: Router, | ||||||
|     private uploadDocumentsService: UploadDocumentsService |     private uploadDocumentsService: UploadDocumentsService, | ||||||
|  |     private tasksService: TasksService | ||||||
|   ) { |   ) { | ||||||
|     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' | ||||||
| @@ -65,6 +67,7 @@ export class AppComponent implements OnInit, OnDestroy { | |||||||
|     this.successSubscription = this.consumerStatusService |     this.successSubscription = this.consumerStatusService | ||||||
|       .onDocumentConsumptionFinished() |       .onDocumentConsumptionFinished() | ||||||
|       .subscribe((status) => { |       .subscribe((status) => { | ||||||
|  |         this.tasksService.reload() | ||||||
|         if ( |         if ( | ||||||
|           this.showNotification(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS) |           this.showNotification(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS) | ||||||
|         ) { |         ) { | ||||||
| @@ -83,6 +86,7 @@ export class AppComponent implements OnInit, OnDestroy { | |||||||
|     this.failedSubscription = this.consumerStatusService |     this.failedSubscription = this.consumerStatusService | ||||||
|       .onDocumentConsumptionFailed() |       .onDocumentConsumptionFailed() | ||||||
|       .subscribe((status) => { |       .subscribe((status) => { | ||||||
|  |         this.tasksService.reload() | ||||||
|         if ( |         if ( | ||||||
|           this.showNotification(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_FAILED) |           this.showNotification(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_FAILED) | ||||||
|         ) { |         ) { | ||||||
| @@ -95,6 +99,7 @@ export class AppComponent implements OnInit, OnDestroy { | |||||||
|     this.newDocumentSubscription = this.consumerStatusService |     this.newDocumentSubscription = this.consumerStatusService | ||||||
|       .onDocumentDetected() |       .onDocumentDetected() | ||||||
|       .subscribe((status) => { |       .subscribe((status) => { | ||||||
|  |         this.tasksService.reload() | ||||||
|         if ( |         if ( | ||||||
|           this.showNotification( |           this.showNotification( | ||||||
|             SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT |             SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT | ||||||
|   | |||||||
| @@ -61,7 +61,7 @@ import { SafeUrlPipe } from './pipes/safeurl.pipe' | |||||||
| import { SafeHtmlPipe } from './pipes/safehtml.pipe' | import { SafeHtmlPipe } from './pipes/safehtml.pipe' | ||||||
| import { CustomDatePipe } from './pipes/custom-date.pipe' | import { CustomDatePipe } from './pipes/custom-date.pipe' | ||||||
| import { DateComponent } from './components/common/input/date/date.component' | import { DateComponent } from './components/common/input/date/date.component' | ||||||
| import { ISODateTimeAdapter } from './utils/ngb-iso-date-time-adapter' | import { ISODateAdapter } from './utils/ngb-iso-date-adapter' | ||||||
| import { LocalizedDateParserFormatter } from './utils/ngb-date-parser-formatter' | import { LocalizedDateParserFormatter } from './utils/ngb-date-parser-formatter' | ||||||
| import { ApiVersionInterceptor } from './interceptors/api-version.interceptor' | import { ApiVersionInterceptor } from './interceptors/api-version.interceptor' | ||||||
| import { ColorSliderModule } from 'ngx-color/slider' | import { ColorSliderModule } from 'ngx-color/slider' | ||||||
| @@ -90,6 +90,7 @@ import localeZh from '@angular/common/locales/zh' | |||||||
| 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 { StoragePathEditDialogComponent } from './components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component' | import { StoragePathEditDialogComponent } from './components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component' | ||||||
| import { SettingsService } from './services/settings.service' | import { SettingsService } from './services/settings.service' | ||||||
|  | import { TasksComponent } from './components/manage/tasks/tasks.component' | ||||||
|  |  | ||||||
| registerLocaleData(localeBe) | registerLocaleData(localeBe) | ||||||
| registerLocaleData(localeCs) | registerLocaleData(localeCs) | ||||||
| @@ -171,6 +172,7 @@ function initializeApp(settings: SettingsService) { | |||||||
|     DateComponent, |     DateComponent, | ||||||
|     ColorComponent, |     ColorComponent, | ||||||
|     DocumentAsnComponent, |     DocumentAsnComponent, | ||||||
|  |     TasksComponent, | ||||||
|   ], |   ], | ||||||
|   imports: [ |   imports: [ | ||||||
|     BrowserModule, |     BrowserModule, | ||||||
| @@ -205,7 +207,7 @@ function initializeApp(settings: SettingsService) { | |||||||
|     }, |     }, | ||||||
|     FilterPipe, |     FilterPipe, | ||||||
|     DocumentTitlePipe, |     DocumentTitlePipe, | ||||||
|     { provide: NgbDateAdapter, useClass: ISODateTimeAdapter }, |     { provide: NgbDateAdapter, useClass: ISODateAdapter }, | ||||||
|     { provide: NgbDateParserFormatter, useClass: LocalizedDateParserFormatter }, |     { provide: NgbDateParserFormatter, useClass: LocalizedDateParserFormatter }, | ||||||
|   ], |   ], | ||||||
|   bootstrap: [AppComponent], |   bootstrap: [AppComponent], | ||||||
|   | |||||||
| @@ -141,6 +141,13 @@ | |||||||
|               </svg> <ng-container i18n>Storage paths</ng-container> |               </svg> <ng-container i18n>Storage paths</ng-container> | ||||||
|             </a> |             </a> | ||||||
|           </li> |           </li> | ||||||
|  |           <li class="nav-item"> | ||||||
|  |             <a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()"> | ||||||
|  |               <svg class="sidebaricon" fill="currentColor"> | ||||||
|  |                 <use xlink:href="assets/bootstrap-icons.svg#list-task"/> | ||||||
|  |               </svg> <ng-container i18n>File Tasks<ng-container *ngIf="tasksService.failedFileTasks.length > 0"><span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span></ng-container></ng-container> | ||||||
|  |             </a> | ||||||
|  |           </li> | ||||||
|           <li class="nav-item"> |           <li class="nav-item"> | ||||||
|             <a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()"> |             <a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()"> | ||||||
|               <svg class="sidebaricon" fill="currentColor"> |               <svg class="sidebaricon" fill="currentColor"> | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import { Component } from '@angular/core' | import { Component } from '@angular/core' | ||||||
| import { FormControl } from '@angular/forms' | import { FormControl } from '@angular/forms' | ||||||
| import { ActivatedRoute, Router, Params } from '@angular/router' | import { ActivatedRoute, Router, Params } from '@angular/router' | ||||||
| import { from, Observable, Subscription, BehaviorSubject } from 'rxjs' | import { from, Observable } from 'rxjs' | ||||||
| import { | import { | ||||||
|   debounceTime, |   debounceTime, | ||||||
|   distinctUntilChanged, |   distinctUntilChanged, | ||||||
| @@ -15,15 +15,14 @@ import { SavedViewService } from 'src/app/services/rest/saved-view.service' | |||||||
| import { SearchService } from 'src/app/services/rest/search.service' | import { SearchService } from 'src/app/services/rest/search.service' | ||||||
| import { environment } from 'src/environments/environment' | import { environment } from 'src/environments/environment' | ||||||
| import { DocumentDetailComponent } from '../document-detail/document-detail.component' | import { DocumentDetailComponent } from '../document-detail/document-detail.component' | ||||||
| import { Meta } from '@angular/platform-browser' |  | ||||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service' | import { DocumentListViewService } from 'src/app/services/document-list-view.service' | ||||||
| import { FILTER_FULLTEXT_QUERY } from 'src/app/data/filter-rule-type' | import { FILTER_FULLTEXT_QUERY } from 'src/app/data/filter-rule-type' | ||||||
| import { | import { | ||||||
|   RemoteVersionService, |   RemoteVersionService, | ||||||
|   AppRemoteVersion, |   AppRemoteVersion, | ||||||
| } from 'src/app/services/rest/remote-version.service' | } from 'src/app/services/rest/remote-version.service' | ||||||
| import { QueryParamsService } from 'src/app/services/query-params.service' |  | ||||||
| import { SettingsService } from 'src/app/services/settings.service' | import { SettingsService } from 'src/app/services/settings.service' | ||||||
|  | import { TasksService } from 'src/app/services/tasks.service' | ||||||
|  |  | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-app-frame', |   selector: 'app-app-frame', | ||||||
| @@ -38,14 +37,16 @@ export class AppFrameComponent { | |||||||
|     private searchService: SearchService, |     private searchService: SearchService, | ||||||
|     public savedViewService: SavedViewService, |     public savedViewService: SavedViewService, | ||||||
|     private remoteVersionService: RemoteVersionService, |     private remoteVersionService: RemoteVersionService, | ||||||
|     private queryParamsService: QueryParamsService, |     private list: DocumentListViewService, | ||||||
|     public settingsService: SettingsService |     public settingsService: SettingsService, | ||||||
|  |     public tasksService: TasksService | ||||||
|   ) { |   ) { | ||||||
|     this.remoteVersionService |     this.remoteVersionService | ||||||
|       .checkForUpdates() |       .checkForUpdates() | ||||||
|       .subscribe((appRemoteVersion: AppRemoteVersion) => { |       .subscribe((appRemoteVersion: AppRemoteVersion) => { | ||||||
|         this.appRemoteVersion = appRemoteVersion |         this.appRemoteVersion = appRemoteVersion | ||||||
|       }) |       }) | ||||||
|  |     tasksService.reload() | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   versionString = `${environment.appTitle} ${environment.version}` |   versionString = `${environment.appTitle} ${environment.version}` | ||||||
| @@ -94,7 +95,7 @@ export class AppFrameComponent { | |||||||
|  |  | ||||||
|   search() { |   search() { | ||||||
|     this.closeMenu() |     this.closeMenu() | ||||||
|     this.queryParamsService.navigateWithFilterRules([ |     this.list.quickFilter([ | ||||||
|       { |       { | ||||||
|         rule_type: FILTER_FULLTEXT_QUERY, |         rule_type: FILTER_FULLTEXT_QUERY, | ||||||
|         value: (this.searchField.value as string).trim(), |         value: (this.searchField.value as string).trim(), | ||||||
|   | |||||||
| @@ -16,13 +16,11 @@ | |||||||
|   <div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> |   <div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> | ||||||
|     <div class="list-group list-group-flush"> |     <div class="list-group list-group-flush"> | ||||||
|       <div *ngIf="!editing && multiple" class="list-group-item d-flex"> |       <div *ngIf="!editing && multiple" class="list-group-item d-flex"> | ||||||
|         <div class="btn-group btn-group-xs btn-group-toggle flex-fill" ngbRadioGroup [(ngModel)]="selectionModel.logicalOperator" (change)="selectionModel.toggleOperator()" [disabled]="!operatorToggleEnabled"> |         <div class="btn-group btn-group-xs flex-fill"> | ||||||
|           <label ngbButtonLabel class="btn btn-outline-primary"> |           <input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!operatorToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorAnd" value="and"> | ||||||
|             <input ngbButton type="radio" class="btn-check" name="logicalOperator" value="and" i18n> All |           <label class="btn btn-outline-primary" for="logicalOperatorAnd" i18n>All</label> | ||||||
|           </label> |           <input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!operatorToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorOr" value="or"> | ||||||
|           <label ngbButtonLabel class="btn btn-outline-primary"> |           <label class="btn btn-outline-primary" for="logicalOperatorOr" i18n>Any</label> | ||||||
|             <input ngbButton type="radio" class="btn-check" name="logicalOperator" value="or" i18n> Any |  | ||||||
|           </label> |  | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|       <div class="list-group-item"> |       <div class="list-group-item"> | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
|   <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 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)" |           (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"> | ||||||
|     <button class="btn btn-outline-secondary calendar" (click)="datePicker.toggle()" type="button"> |     <button class="btn btn-outline-secondary calendar" (click)="datePicker.toggle()" type="button"> | ||||||
|       <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"> | ||||||
|   | |||||||
| @@ -1,6 +1,8 @@ | |||||||
| import { Component, forwardRef, OnInit } from '@angular/core' | import { Component, forwardRef, OnInit } from '@angular/core' | ||||||
| import { NG_VALUE_ACCESSOR } from '@angular/forms' | import { NG_VALUE_ACCESSOR } from '@angular/forms' | ||||||
|  | import { NgbDateParserFormatter } from '@ng-bootstrap/ng-bootstrap' | ||||||
| import { SettingsService } from 'src/app/services/settings.service' | import { SettingsService } from 'src/app/services/settings.service' | ||||||
|  | import { LocalizedDateParserFormatter } from 'src/app/utils/ngb-date-parser-formatter' | ||||||
| import { AbstractInputComponent } from '../abstract-input' | import { AbstractInputComponent } from '../abstract-input' | ||||||
|  |  | ||||||
| @Component({ | @Component({ | ||||||
| @@ -19,7 +21,10 @@ export class DateComponent | |||||||
|   extends AbstractInputComponent<string> |   extends AbstractInputComponent<string> | ||||||
|   implements OnInit |   implements OnInit | ||||||
| { | { | ||||||
|   constructor(private settings: SettingsService) { |   constructor( | ||||||
|  |     private settings: SettingsService, | ||||||
|  |     private ngbDateParserFormatter: NgbDateParserFormatter | ||||||
|  |   ) { | ||||||
|     super() |     super() | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -30,7 +35,20 @@ export class DateComponent | |||||||
|  |  | ||||||
|   placeholder: string |   placeholder: string | ||||||
|  |  | ||||||
|   // prevent chars other than numbers and separators |   onPaste(event: ClipboardEvent) { | ||||||
|  |     const clipboardData: DataTransfer = | ||||||
|  |       event.clipboardData || window['clipboardData'] | ||||||
|  |     if (clipboardData) { | ||||||
|  |       event.preventDefault() | ||||||
|  |       let pastedText = clipboardData.getData('text') | ||||||
|  |       pastedText = pastedText.replace(/[\sa-z#!$%\^&\*;:{}=\-_`~()]+/g, '') | ||||||
|  |       const parsedDate = this.ngbDateParserFormatter.parse(pastedText) | ||||||
|  |       const formattedDate = this.ngbDateParserFormatter.format(parsedDate) | ||||||
|  |       this.writeValue(formattedDate) | ||||||
|  |       this.onChange(formattedDate) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   onKeyPress(event: KeyboardEvent) { |   onKeyPress(event: KeyboardEvent) { | ||||||
|     if ('Enter' !== event.key && !/[0-9,\.\/-]+/.test(event.key)) { |     if ('Enter' !== event.key && !/[0-9,\.\/-]+/.test(event.key)) { | ||||||
|       event.preventDefault() |       event.preventDefault() | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
| a { | a { | ||||||
|     cursor: pointer; |     cursor: pointer; | ||||||
|  |     white-space: normal; | ||||||
|  |     word-break: break-word; | ||||||
|  |     text-align: end; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,5 +4,5 @@ | |||||||
|   [class]="toast.classname" |   [class]="toast.classname" | ||||||
|   (hidden)="toastService.closeToast(toast)"> |   (hidden)="toastService.closeToast(toast)"> | ||||||
|   <p>{{toast.content}}</p> |   <p>{{toast.content}}</p> | ||||||
|   <p *ngIf="toast.action"><button class="btn btn-sm btn-outline-secondary" (click)="toastService.closeToast(toast); toast.action()">{{toast.actionName}}</button></p> |   <p class="mb-0" *ngIf="toast.action"><button class="btn btn-sm btn-outline-secondary" (click)="toastService.closeToast(toast); toast.action()">{{toast.actionName}}</button></p> | ||||||
| </ngb-toast> | </ngb-toast> | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ | |||||||
|     </thead> |     </thead> | ||||||
|     <tbody> |     <tbody> | ||||||
|       <tr *ngFor="let doc of documents" (click)="openDocumentsService.openDocument(doc)"> |       <tr *ngFor="let doc of documents" (click)="openDocumentsService.openDocument(doc)"> | ||||||
|         <td>{{doc.created | customDate}}</td> |         <td>{{doc.created_date | customDate}}</td> | ||||||
|         <td>{{doc.title | documentTitle}}<app-tag [tag]="t" *ngFor="let t of doc.tags$ | async" class="ms-1" (click)="clickTag(t); $event.stopPropagation();"></app-tag></td> |         <td>{{doc.title | documentTitle}}<app-tag [tag]="t" *ngFor="let t of doc.tags$ | async" class="ms-1" (click)="clickTag(t); $event.stopPropagation();"></app-tag></td> | ||||||
|       </tr> |       </tr> | ||||||
|     </tbody> |     </tbody> | ||||||
|   | |||||||
| @@ -7,8 +7,8 @@ import { ConsumerStatusService } from 'src/app/services/consumer-status.service' | |||||||
| import { DocumentService } from 'src/app/services/rest/document.service' | import { DocumentService } from 'src/app/services/rest/document.service' | ||||||
| import { PaperlessTag } from 'src/app/data/paperless-tag' | 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 { QueryParamsService } from 'src/app/services/query-params.service' |  | ||||||
| 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' | ||||||
|  |  | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-saved-view-widget', |   selector: 'app-saved-view-widget', | ||||||
| @@ -21,7 +21,7 @@ export class SavedViewWidgetComponent implements OnInit, OnDestroy { | |||||||
|   constructor( |   constructor( | ||||||
|     private documentService: DocumentService, |     private documentService: DocumentService, | ||||||
|     private router: Router, |     private router: Router, | ||||||
|     private queryParamsService: QueryParamsService, |     private list: DocumentListViewService, | ||||||
|     private consumerStatusService: ConsumerStatusService, |     private consumerStatusService: ConsumerStatusService, | ||||||
|     public openDocumentsService: OpenDocumentsService |     public openDocumentsService: OpenDocumentsService | ||||||
|   ) {} |   ) {} | ||||||
| @@ -47,7 +47,7 @@ export class SavedViewWidgetComponent implements OnInit, OnDestroy { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   reload() { |   reload() { | ||||||
|     this.loading = true |     this.loading = this.documents.length == 0 | ||||||
|     this.documentService |     this.documentService | ||||||
|       .listFiltered( |       .listFiltered( | ||||||
|         1, |         1, | ||||||
| @@ -73,7 +73,7 @@ export class SavedViewWidgetComponent implements OnInit, OnDestroy { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   clickTag(tag: PaperlessTag) { |   clickTag(tag: PaperlessTag) { | ||||||
|     this.queryParamsService.navigateWithFilterRules([ |     this.list.quickFilter([ | ||||||
|       { rule_type: FILTER_HAS_TAGS_ALL, value: tag.id.toString() }, |       { rule_type: FILTER_HAS_TAGS_ALL, value: tag.id.toString() }, | ||||||
|     ]) |     ]) | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -68,7 +68,7 @@ | |||||||
|  |  | ||||||
|                         <app-input-text #inputTitle i18n-title title="Title" formControlName="title" (keyup)="titleKeyUp($event)" [error]="error?.title"></app-input-text> |                         <app-input-text #inputTitle i18n-title title="Title" formControlName="title" (keyup)="titleKeyUp($event)" [error]="error?.title"></app-input-text> | ||||||
|                         <app-input-number i18n-title title="Archive serial number" [error]="error?.archive_serial_number" formControlName='archive_serial_number'></app-input-number> |                         <app-input-number i18n-title title="Archive serial number" [error]="error?.archive_serial_number" formControlName='archive_serial_number'></app-input-number> | ||||||
|                         <app-input-date i18n-title title="Date created" formControlName="created" [error]="error?.created"></app-input-date> |                         <app-input-date i18n-title title="Date created" formControlName="created_date" [error]="error?.created_date"></app-input-date> | ||||||
|                         <app-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true" |                         <app-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true" | ||||||
|                             (createNew)="createCorrespondent($event)" [suggestions]="suggestions?.correspondents"></app-input-select> |                             (createNew)="createCorrespondent($event)" [suggestions]="suggestions?.correspondents"></app-input-select> | ||||||
|                         <app-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true" |                         <app-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true" | ||||||
|   | |||||||
| @@ -14,10 +14,14 @@ | |||||||
| } | } | ||||||
|  |  | ||||||
| ::ng-deep .ng2-pdf-viewer-container .page { | ::ng-deep .ng2-pdf-viewer-container .page { | ||||||
|   --page-margin: 1px 0 -8px; |   --page-margin: 1px 0 10px; | ||||||
|   width: 100% !important; |   width: 100% !important; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | ::ng-deep .ng2-pdf-viewer-container .page:last-child { | ||||||
|  |   --page-margin: 1px 0 20px; | ||||||
|  | } | ||||||
|  |  | ||||||
| .password-prompt { | .password-prompt { | ||||||
|   position: absolute; |   position: absolute; | ||||||
|   top: 30%; |   top: 30%; | ||||||
|   | |||||||
| @@ -31,8 +31,6 @@ import { | |||||||
| } from 'rxjs/operators' | } from 'rxjs/operators' | ||||||
| import { PaperlessDocumentSuggestions } from 'src/app/data/paperless-document-suggestions' | import { PaperlessDocumentSuggestions } from 'src/app/data/paperless-document-suggestions' | ||||||
| import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type' | import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type' | ||||||
| import { normalizeDateStr } from 'src/app/utils/date' |  | ||||||
| import { QueryParamsService } from 'src/app/services/query-params.service' |  | ||||||
| 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 { 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' | ||||||
| @@ -74,7 +72,7 @@ export class DocumentDetailComponent | |||||||
|   documentForm: FormGroup = new FormGroup({ |   documentForm: FormGroup = new FormGroup({ | ||||||
|     title: new FormControl(''), |     title: new FormControl(''), | ||||||
|     content: new FormControl(''), |     content: new FormControl(''), | ||||||
|     created: new FormControl(), |     created_date: new FormControl(), | ||||||
|     correspondent: new FormControl(), |     correspondent: new FormControl(), | ||||||
|     document_type: new FormControl(), |     document_type: new FormControl(), | ||||||
|     storage_path: new FormControl(), |     storage_path: new FormControl(), | ||||||
| @@ -120,8 +118,7 @@ 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 queryParamsService: QueryParamsService |  | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   titleKeyUp(event) { |   titleKeyUp(event) { | ||||||
| @@ -141,27 +138,8 @@ export class DocumentDetailComponent | |||||||
|   ngOnInit(): void { |   ngOnInit(): void { | ||||||
|     this.documentForm.valueChanges |     this.documentForm.valueChanges | ||||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) |       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||||
|       .subscribe((changes) => { |       .subscribe(() => { | ||||||
|         this.error = null |         this.error = null | ||||||
|         if (this.ogDate) { |  | ||||||
|           try { |  | ||||||
|             let newDate = new Date(normalizeDateStr(changes['created'])) |  | ||||||
|             newDate.setHours( |  | ||||||
|               this.ogDate.getHours(), |  | ||||||
|               this.ogDate.getMinutes(), |  | ||||||
|               this.ogDate.getSeconds(), |  | ||||||
|               this.ogDate.getMilliseconds() |  | ||||||
|             ) |  | ||||||
|             this.documentForm.patchValue( |  | ||||||
|               { created: newDate.toISOString() }, |  | ||||||
|               { emitEvent: false } |  | ||||||
|             ) |  | ||||||
|           } catch (e) { |  | ||||||
|             // catch this before we try to save and simulate an api error |  | ||||||
|             this.error = { created: e.message } |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         Object.assign(this.document, this.documentForm.value) |         Object.assign(this.document, this.documentForm.value) | ||||||
|       }) |       }) | ||||||
|  |  | ||||||
| @@ -233,13 +211,11 @@ export class DocumentDetailComponent | |||||||
|               }, |               }, | ||||||
|             }) |             }) | ||||||
|  |  | ||||||
|           this.ogDate = new Date(normalizeDateStr(doc.created.toString())) |  | ||||||
|  |  | ||||||
|           // Initialize dirtyCheck |           // Initialize dirtyCheck | ||||||
|           this.store = new BehaviorSubject({ |           this.store = new BehaviorSubject({ | ||||||
|             title: doc.title, |             title: doc.title, | ||||||
|             content: doc.content, |             content: doc.content, | ||||||
|             created: this.ogDate.toISOString(), |             created_date: doc.created_date, | ||||||
|             correspondent: doc.correspondent, |             correspondent: doc.correspondent, | ||||||
|             document_type: doc.document_type, |             document_type: doc.document_type, | ||||||
|             storage_path: doc.storage_path, |             storage_path: doc.storage_path, | ||||||
| @@ -247,12 +223,6 @@ export class DocumentDetailComponent | |||||||
|             tags: [...doc.tags], |             tags: [...doc.tags], | ||||||
|           }) |           }) | ||||||
|  |  | ||||||
|           // start with ISO8601 string |  | ||||||
|           this.documentForm.patchValue( |  | ||||||
|             { created: this.ogDate.toISOString() }, |  | ||||||
|             { emitEvent: false } |  | ||||||
|           ) |  | ||||||
|  |  | ||||||
|           this.isDirty$ = dirtyCheck( |           this.isDirty$ = dirtyCheck( | ||||||
|             this.documentForm, |             this.documentForm, | ||||||
|             this.store.asObservable() |             this.store.asObservable() | ||||||
| @@ -494,7 +464,7 @@ export class DocumentDetailComponent | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   moreLike() { |   moreLike() { | ||||||
|     this.queryParamsService.navigateWithFilterRules([ |     this.documentListViewService.quickFilter([ | ||||||
|       { |       { | ||||||
|         rule_type: FILTER_FULLTEXT_MORELIKE, |         rule_type: FILTER_FULLTEXT_MORELIKE, | ||||||
|         value: this.documentId.toString(), |         value: this.documentId.toString(), | ||||||
|   | |||||||
| @@ -66,23 +66,30 @@ | |||||||
|   </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-group btn-group-sm me-2"> | ||||||
|       <button type="button" [disabled]="awaitingDownload" class="btn btn-outline-primary btn-sm" (click)="downloadSelected()"> |  | ||||||
|         <svg *ngIf="!awaitingDownload" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> |       <div ngbDropdown class="me-2 d-flex"> | ||||||
|           <use xlink:href="assets/bootstrap-icons.svg#download" /> |         <button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle> | ||||||
|         </svg> |           <svg class="toolbaricon" fill="currentColor"> | ||||||
|         <div *ngIf="awaitingDownload" class="spinner-border spinner-border-sm" role="status"> |             <use xlink:href="assets/bootstrap-icons.svg#three-dots" /> | ||||||
|           <span class="visually-hidden">Preparing download...</span> |           </svg> | ||||||
|         </div> |           <div class="d-none d-sm-inline"> <ng-container i18n>Actions</ng-container></div> | ||||||
|           |         </button> | ||||||
|         <ng-container i18n>Download</ng-container> |         <div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow"> | ||||||
|       </button> |           <button ngbDropdownItem [disabled]="awaitingDownload" (click)="downloadSelected()" i18n> | ||||||
|       <div class="btn-group" ngbDropdown role="group" aria-label="Button group with nested dropdown"> |             Download | ||||||
|         <button [disabled]="awaitingDownload" class="btn btn-outline-primary btn-sm dropdown-toggle-split" ngbDropdownToggle></button> |             <div *ngIf="awaitingDownload" class="spinner-border spinner-border-sm" role="status"> | ||||||
|         <div class="dropdown-menu shadow" ngbDropdownMenu> |               <span class="visually-hidden">Preparing download...</span> | ||||||
|           <button ngbDropdownItem i18n (click)="downloadSelected('originals')">Download originals</button> |             </div> | ||||||
|  |           </button> | ||||||
|  |           <button ngbDropdownItem [disabled]="awaitingDownload" (click)="downloadSelected('originals')" i18n> | ||||||
|  |             Download originals | ||||||
|  |             <div *ngIf="awaitingDownload" class="spinner-border spinner-border-sm" role="status"> | ||||||
|  |               <span class="visually-hidden">Preparing download...</span> | ||||||
|  |             </div> | ||||||
|  |           </button> | ||||||
|  |           <button ngbDropdownItem (click)="redoOcrSelected()" i18n>Redo OCR</button> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |  | ||||||
|  |  | ||||||
|     <button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()"> |     <button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()"> | ||||||
|       <svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> |       <svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> | ||||||
|   | |||||||
| @@ -379,4 +379,19 @@ export class BulkEditorComponent { | |||||||
|         this.awaitingDownload = false |         this.awaitingDownload = false | ||||||
|       }) |       }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   redoOcrSelected() { | ||||||
|  |     let modal = this.modalService.open(ConfirmDialogComponent, { | ||||||
|  |       backdrop: 'static', | ||||||
|  |     }) | ||||||
|  |     modal.componentInstance.title = $localize`Redo OCR confirm` | ||||||
|  |     modal.componentInstance.messageBold = $localize`This operation will permanently redo OCR for ${this.list.selected.size} selected document(s).` | ||||||
|  |     modal.componentInstance.message = $localize`This operation cannot be undone.` | ||||||
|  |     modal.componentInstance.btnClass = 'btn-danger' | ||||||
|  |     modal.componentInstance.btnCaption = $localize`Proceed` | ||||||
|  |     modal.componentInstance.confirmClicked.subscribe(() => { | ||||||
|  |       modal.componentInstance.buttonsEnabled = false | ||||||
|  |       this.executeBulkOperation(modal, 'redo_ocr', {}) | ||||||
|  |     }) | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -92,7 +92,7 @@ | |||||||
|                 <path d="M11 6.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1z"/> |                 <path d="M11 6.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1z"/> | ||||||
|                 <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> | ||||||
|               <small>{{document.created | customDate:'mediumDate'}}</small> |               <small>{{document.created_date | customDate:'mediumDate'}}</small> | ||||||
|             </div> |             </div> | ||||||
|             <div *ngIf="document.__search_hit__?.score" class="list-group-item bg-light text-dark border-0 d-flex p-0 ps-4 search-score"> |             <div *ngIf="document.__search_hit__?.score" class="list-group-item bg-light text-dark border-0 d-flex p-0 ps-4 search-score"> | ||||||
|               <small class="text-muted" i18n>Score:</small> |               <small class="text-muted" i18n>Score:</small> | ||||||
|   | |||||||
| @@ -10,10 +10,8 @@ | |||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <div style="top: 0; right: 0; font-size: large" class="text-end position-absolute me-1"> |       <div class="tags d-flex flex-column text-end position-absolute me-1 fs-6"> | ||||||
|         <div *ngFor="let t of getTagsLimited$() | async"> |         <app-tag *ngFor="let t of getTagsLimited$() | async" [tag]="t" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="true" linkTitle="Toggle tag filter" i18n-linkTitle></app-tag> | ||||||
|           <app-tag [tag]="t" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="true" linkTitle="Filter by tag" i18n-linkTitle></app-tag> |  | ||||||
|         </div> |  | ||||||
|         <div *ngIf="moreTags"> |         <div *ngIf="moreTags"> | ||||||
|           <span class="badge badge-secondary">+ {{moreTags}}</span> |           <span class="badge badge-secondary">+ {{moreTags}}</span> | ||||||
|         </div> |         </div> | ||||||
| @@ -23,21 +21,21 @@ | |||||||
|     <div class="card-body p-2"> |     <div class="card-body p-2"> | ||||||
|       <p class="card-text"> |       <p class="card-text"> | ||||||
|         <ng-container *ngIf="document.correspondent"> |         <ng-container *ngIf="document.correspondent"> | ||||||
|           <a title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name}}</a>: |           <a title="Toggle correspondent filter" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name}}</a>: | ||||||
|         </ng-container> |         </ng-container> | ||||||
|         {{document.title | documentTitle}} |         {{document.title | documentTitle}} | ||||||
|       </p> |       </p> | ||||||
|     </div> |     </div> | ||||||
|     <div class="card-footer pt-0 pb-2 px-2"> |     <div class="card-footer pt-0 pb-2 px-2"> | ||||||
|       <div class="list-group list-group-flush border-0 pt-1 pb-2 card-info"> |       <div class="list-group list-group-flush border-0 pt-1 pb-2 card-info"> | ||||||
|         <button *ngIf="document.document_type" type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Filter by document type" i18n-title |         <button *ngIf="document.document_type" type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle document type filter" i18n-title | ||||||
|          (click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()"> |          (click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()"> | ||||||
|           <svg class="metadata-icon me-2 text-muted bi bi-file-earmark" viewBox="0 0 16 16" fill="currentColor"> |           <svg class="metadata-icon me-2 text-muted bi bi-file-earmark" viewBox="0 0 16 16" fill="currentColor"> | ||||||
|             <path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/> |             <path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/> | ||||||
|           </svg> |           </svg> | ||||||
|           <small>{{(document.document_type$ | async)?.name}}</small> |           <small>{{(document.document_type$ | async)?.name}}</small> | ||||||
|         </button> |         </button> | ||||||
|         <button *ngIf="document.storage_path" type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Filter by storage path" i18n-title |         <button *ngIf="document.storage_path" type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle storage path filter" i18n-title | ||||||
|          (click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()"> |          (click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()"> | ||||||
|           <svg class="metadata-icon me-2 text-muted bi bi-folder" viewBox="0 0 16 16" fill="currentColor"> |           <svg class="metadata-icon me-2 text-muted bi bi-folder" viewBox="0 0 16 16" fill="currentColor"> | ||||||
|             <path d="M0 2a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a2.5 2.5 0 0 1-2.5 2.5h-9A2.5 2.5 0 0 1 1 12.5V5a1 1 0 0 1-1-1V2zm2 3v7.5A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5V5H2zm13-3H1v2h14V2zM5 7.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z"/> |             <path d="M0 2a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a2.5 2.5 0 0 1-2.5 2.5h-9A2.5 2.5 0 0 1 1 12.5V5a1 1 0 0 1-1-1V2zm2 3v7.5A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5V5H2zm13-3H1v2h14V2zM5 7.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z"/> | ||||||
| @@ -57,7 +55,7 @@ | |||||||
|               <path d="M11 6.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1z"/> |               <path d="M11 6.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1z"/> | ||||||
|               <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> | ||||||
|             <small>{{document.created | customDate:'mediumDate'}}</small> |             <small>{{document.created_date | customDate:'mediumDate'}}</small> | ||||||
|           </div> |           </div> | ||||||
|           <div *ngIf="document.archive_serial_number" class="ps-0 p-1"> |           <div *ngIf="document.archive_serial_number" class="ps-0 p-1"> | ||||||
|             <svg class="metadata-icon me-2 text-muted bi bi-upc-scan" viewBox="0 0 16 16" fill="currentColor"> |             <svg class="metadata-icon me-2 text-muted bi bi-upc-scan" viewBox="0 0 16 16" fill="currentColor"> | ||||||
|   | |||||||
| @@ -78,3 +78,11 @@ | |||||||
| a { | a { | ||||||
|   cursor: pointer; |   cursor: pointer; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .tags { | ||||||
|  |   top: 0; | ||||||
|  |   right: 0; | ||||||
|  |   max-width: 80%; | ||||||
|  |   row-gap: .2rem; | ||||||
|  |   line-height: 1; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -13,23 +13,21 @@ | |||||||
|       <button ngbDropdownItem (click)="list.selectAll()" i18n>Select all</button> |       <button ngbDropdownItem (click)="list.selectAll()" i18n>Select all</button> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
|  |   <div class="btn-group flex-fill" role="group"> | ||||||
|   <div class="btn-group btn-group-toggle flex-fill" ngbRadioGroup [(ngModel)]="displayMode" |     <input type="radio" class="btn-check" [(ngModel)]="displayMode" value="details" (ngModelChange)="saveDisplayMode()" id="displayModeDetails"> | ||||||
|     (ngModelChange)="saveDisplayMode()"> |     <label for="displayModeDetails" class="btn btn-outline-primary btn-sm"> | ||||||
|     <label ngbButtonLabel class="btn-outline-primary btn-sm"> |  | ||||||
|       <input ngbButton type="radio" class="btn-check btn-sm" value="details"> |  | ||||||
|       <svg class="toolbaricon" fill="currentColor"> |       <svg class="toolbaricon" fill="currentColor"> | ||||||
|         <use xlink:href="assets/bootstrap-icons.svg#list-ul" /> |         <use xlink:href="assets/bootstrap-icons.svg#list-ul" /> | ||||||
|       </svg> |       </svg> | ||||||
|     </label> |     </label> | ||||||
|     <label ngbButtonLabel class="btn-outline-primary btn-sm"> |     <input type="radio" class="btn-check" [(ngModel)]="displayMode" value="smallCards" (ngModelChange)="saveDisplayMode()" id="displayModeSmall"> | ||||||
|       <input ngbButton type="radio" class="btn-check btn-sm" value="smallCards"> |     <label for="displayModeSmall" class="btn btn-outline-primary btn-sm"> | ||||||
|       <svg class="toolbaricon" fill="currentColor"> |       <svg class="toolbaricon" fill="currentColor"> | ||||||
|         <use xlink:href="assets/bootstrap-icons.svg#grid" /> |         <use xlink:href="assets/bootstrap-icons.svg#grid" /> | ||||||
|       </svg> |       </svg> | ||||||
|     </label> |     </label> | ||||||
|     <label ngbButtonLabel class="btn-outline-primary btn-sm"> |     <input type="radio" class="btn-check" [(ngModel)]="displayMode" value="largeCards" (ngModelChange)="saveDisplayMode()" id="displayModeLarge"> | ||||||
|       <input ngbButton type="radio" class="btn-check btn-sm" value="largeCards"> |     <label for="displayModeLarge" class="btn btn-outline-primary btn-sm"> | ||||||
|       <svg class="toolbaricon" fill="currentColor"> |       <svg class="toolbaricon" fill="currentColor"> | ||||||
|         <use xlink:href="assets/bootstrap-icons.svg#hdd-stack" /> |         <use xlink:href="assets/bootstrap-icons.svg#hdd-stack" /> | ||||||
|       </svg> |       </svg> | ||||||
| @@ -39,15 +37,15 @@ | |||||||
|   <div ngbDropdown class="btn-group ms-2 flex-fill"> |   <div ngbDropdown class="btn-group ms-2 flex-fill"> | ||||||
|     <button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle i18n>Sort</button> |     <button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle i18n>Sort</button> | ||||||
|     <div ngbDropdownMenu aria-labelledby="dropdownBasic1" class="shadow dropdown-menu-right"> |     <div ngbDropdownMenu aria-labelledby="dropdownBasic1" class="shadow dropdown-menu-right"> | ||||||
|       <div class="w-100 d-flex btn-group-toggle pb-2 mb-1 border-bottom" ngbRadioGroup [(ngModel)]="listSort"> |       <div class="w-100 d-flex pb-2 mb-1 border-bottom"> | ||||||
|         <label ngbButtonLabel class="btn-outline-primary btn-sm mx-2 flex-fill"> |         <input type="radio" class="btn-check" [value]="false" [(ngModel)]="listSortReverse" id="listSortReverseFalse"> | ||||||
|           <input ngbButton type="radio" class="btn btn-check btn-sm" [value]="false"> |         <label class="btn btn-outline-primary btn-sm mx-2 flex-fill" for="listSortReverseFalse"> | ||||||
|           <svg class="toolbaricon" fill="currentColor"> |           <svg class="toolbaricon" fill="currentColor"> | ||||||
|             <use xlink:href="assets/bootstrap-icons.svg#sort-alpha-down" /> |             <use xlink:href="assets/bootstrap-icons.svg#sort-alpha-down" /> | ||||||
|           </svg> |           </svg> | ||||||
|         </label> |         </label> | ||||||
|         <label ngbButtonLabel class="btn-outline-primary btn-sm me-2 flex-fill"> |         <input type="radio" class="btn-check" [value]="true" [(ngModel)]="listSortReverse" id="listSortReverseTrue"> | ||||||
|           <input ngbButton type="radio" class="btn btn-check btn-sm" [value]="true"> |         <label class="btn btn-outline-primary btn-sm me-2 flex-fill" for="listSortReverseTrue"> | ||||||
|           <svg class="toolbaricon" fill="currentColor"> |           <svg class="toolbaricon" fill="currentColor"> | ||||||
|             <use xlink:href="assets/bootstrap-icons.svg#sort-alpha-up-alt" /> |             <use xlink:href="assets/bootstrap-icons.svg#sort-alpha-up-alt" /> | ||||||
|           </svg> |           </svg> | ||||||
| @@ -93,7 +91,7 @@ | |||||||
|         <span i18n *ngIf="list.selected.size == 0">{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</span> <span i18n *ngIf="isFiltered">(filtered)</span> |         <span i18n *ngIf="list.selected.size == 0">{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</span> <span i18n *ngIf="isFiltered">(filtered)</span> | ||||||
|       </ng-container> |       </ng-container> | ||||||
|     </p> |     </p> | ||||||
|     <ngb-pagination [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5" |     <ngb-pagination [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" (pageChange)="setPage($event)" [page]="list.currentPage" [maxSize]="5" | ||||||
|     [rotate]="true" aria-label="Default pagination"></ngb-pagination> |     [rotate]="true" aria-label="Default pagination"></ngb-pagination> | ||||||
|   </div> |   </div> | ||||||
| </ng-template> | </ng-template> | ||||||
| @@ -188,7 +186,7 @@ | |||||||
|           </ng-container> |           </ng-container> | ||||||
|         </td> |         </td> | ||||||
|         <td> |         <td> | ||||||
|           {{d.created | customDate}} |           {{d.created_date | customDate}} | ||||||
|         </td> |         </td> | ||||||
|         <td class="d-none d-xl-table-cell"> |         <td class="d-none d-xl-table-cell"> | ||||||
|           {{d.added | customDate}} |           {{d.added | customDate}} | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import { | import { | ||||||
|   AfterViewInit, |  | ||||||
|   Component, |   Component, | ||||||
|   OnDestroy, |   OnDestroy, | ||||||
|   OnInit, |   OnInit, | ||||||
| @@ -21,7 +20,6 @@ import { | |||||||
| import { ConsumerStatusService } from 'src/app/services/consumer-status.service' | import { ConsumerStatusService } from 'src/app/services/consumer-status.service' | ||||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service' | import { DocumentListViewService } from 'src/app/services/document-list-view.service' | ||||||
| import { OpenDocumentsService } from 'src/app/services/open-documents.service' | import { OpenDocumentsService } from 'src/app/services/open-documents.service' | ||||||
| import { QueryParamsService } from 'src/app/services/query-params.service' |  | ||||||
| import { | import { | ||||||
|   DOCUMENT_SORT_FIELDS, |   DOCUMENT_SORT_FIELDS, | ||||||
|   DOCUMENT_SORT_FIELDS_FULLTEXT, |   DOCUMENT_SORT_FIELDS_FULLTEXT, | ||||||
| @@ -36,7 +34,7 @@ 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, AfterViewInit { | export class DocumentListComponent implements OnInit, OnDestroy { | ||||||
|   constructor( |   constructor( | ||||||
|     public list: DocumentListViewService, |     public list: DocumentListViewService, | ||||||
|     public savedViewService: SavedViewService, |     public savedViewService: SavedViewService, | ||||||
| @@ -45,7 +43,6 @@ export class DocumentListComponent implements OnInit, OnDestroy, AfterViewInit { | |||||||
|     private toastService: ToastService, |     private toastService: ToastService, | ||||||
|     private modalService: NgbModal, |     private modalService: NgbModal, | ||||||
|     private consumerStatusService: ConsumerStatusService, |     private consumerStatusService: ConsumerStatusService, | ||||||
|     private queryParamsService: QueryParamsService, |  | ||||||
|     public openDocumentsService: OpenDocumentsService |     public openDocumentsService: OpenDocumentsService | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
| @@ -74,26 +71,24 @@ export class DocumentListComponent implements OnInit, OnDestroy, AfterViewInit { | |||||||
|       : DOCUMENT_SORT_FIELDS |       : DOCUMENT_SORT_FIELDS | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   set listSort(reverse: boolean) { |   set listSortReverse(reverse: boolean) { | ||||||
|     this.list.sortReverse = reverse |     this.list.sortReverse = reverse | ||||||
|     this.queryParamsService.sortField = this.list.sortField |  | ||||||
|     this.queryParamsService.sortReverse = reverse |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get listSort(): boolean { |   get listSortReverse(): boolean { | ||||||
|     return this.list.sortReverse |     return this.list.sortReverse | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   setSortField(field: string) { |   setSortField(field: string) { | ||||||
|     this.list.sortField = field |     this.list.sortField = field | ||||||
|     this.queryParamsService.sortField = field |  | ||||||
|     this.queryParamsService.sortReverse = this.listSort |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   onSort(event: SortEvent) { |   onSort(event: SortEvent) { | ||||||
|     this.list.setSort(event.column, event.reverse) |     this.list.setSort(event.column, event.reverse) | ||||||
|     this.queryParamsService.sortField = event.column |   } | ||||||
|     this.queryParamsService.sortReverse = event.reverse |  | ||||||
|  |   setPage(page: number) { | ||||||
|  |     this.list.currentPage = page | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get isBulkEditing(): boolean { |   get isBulkEditing(): boolean { | ||||||
| @@ -133,7 +128,6 @@ export class DocumentListComponent implements OnInit, OnDestroy, AfterViewInit { | |||||||
|         } |         } | ||||||
|         this.list.activateSavedView(view) |         this.list.activateSavedView(view) | ||||||
|         this.list.reload() |         this.list.reload() | ||||||
|         this.queryParamsService.updateFromView(view) |  | ||||||
|         this.unmodifiedFilterRules = view.filter_rules |         this.unmodifiedFilterRules = view.filter_rules | ||||||
|       }) |       }) | ||||||
|  |  | ||||||
| @@ -148,22 +142,12 @@ export class DocumentListComponent implements OnInit, OnDestroy, AfterViewInit { | |||||||
|           this.loadViewConfig(parseInt(queryParams.get('view'))) |           this.loadViewConfig(parseInt(queryParams.get('view'))) | ||||||
|         } else { |         } else { | ||||||
|           this.list.activateSavedView(null) |           this.list.activateSavedView(null) | ||||||
|           this.queryParamsService.parseQueryParams(queryParams) |           this.list.loadFromQueryParams(queryParams) | ||||||
|           this.unmodifiedFilterRules = [] |           this.unmodifiedFilterRules = [] | ||||||
|         } |         } | ||||||
|       }) |       }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   ngAfterViewInit(): void { |  | ||||||
|     this.filterEditor.filterRulesChange |  | ||||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) |  | ||||||
|       .subscribe({ |  | ||||||
|         next: (filterRules) => { |  | ||||||
|           this.queryParamsService.updateFilterRules(filterRules) |  | ||||||
|         }, |  | ||||||
|       }) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   ngOnDestroy() { |   ngOnDestroy() { | ||||||
|     // unsubscribes all |     // unsubscribes all | ||||||
|     this.unsubscribeNotifier.next(this) |     this.unsubscribeNotifier.next(this) | ||||||
| @@ -175,9 +159,8 @@ export class DocumentListComponent implements OnInit, OnDestroy, AfterViewInit { | |||||||
|       .getCached(viewId) |       .getCached(viewId) | ||||||
|       .pipe(first()) |       .pipe(first()) | ||||||
|       .subscribe((view) => { |       .subscribe((view) => { | ||||||
|         this.list.loadSavedView(view) |         this.list.activateSavedView(view) | ||||||
|         this.list.reload() |         this.list.reload() | ||||||
|         this.queryParamsService.updateFromView(view) |  | ||||||
|       }) |       }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -246,34 +229,26 @@ export class DocumentListComponent implements OnInit, OnDestroy, AfterViewInit { | |||||||
|  |  | ||||||
|   clickTag(tagID: number) { |   clickTag(tagID: number) { | ||||||
|     this.list.selectNone() |     this.list.selectNone() | ||||||
|     setTimeout(() => { |     this.filterEditor.toggleTag(tagID) | ||||||
|       this.filterEditor.addTag(tagID) |  | ||||||
|     }) |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   clickCorrespondent(correspondentID: number) { |   clickCorrespondent(correspondentID: number) { | ||||||
|     this.list.selectNone() |     this.list.selectNone() | ||||||
|     setTimeout(() => { |     this.filterEditor.toggleCorrespondent(correspondentID) | ||||||
|       this.filterEditor.addCorrespondent(correspondentID) |  | ||||||
|     }) |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   clickDocumentType(documentTypeID: number) { |   clickDocumentType(documentTypeID: number) { | ||||||
|     this.list.selectNone() |     this.list.selectNone() | ||||||
|     setTimeout(() => { |     this.filterEditor.toggleDocumentType(documentTypeID) | ||||||
|       this.filterEditor.addDocumentType(documentTypeID) |  | ||||||
|     }) |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   clickStoragePath(storagePathID: number) { |   clickStoragePath(storagePathID: number) { | ||||||
|     this.list.selectNone() |     this.list.selectNone() | ||||||
|     setTimeout(() => { |     this.filterEditor.toggleStoragePath(storagePathID) | ||||||
|       this.filterEditor.addStoragePath(storagePathID) |  | ||||||
|     }) |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   clickMoreLike(documentID: number) { |   clickMoreLike(documentID: number) { | ||||||
|     this.queryParamsService.navigateWithFilterRules([ |     this.list.quickFilter([ | ||||||
|       { rule_type: FILTER_FULLTEXT_MORELIKE, value: documentID.toString() }, |       { rule_type: FILTER_FULLTEXT_MORELIKE, value: documentID.toString() }, | ||||||
|     ]) |     ]) | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ import { debounceTime, distinctUntilChanged, filter } from 'rxjs/operators' | |||||||
| import { DocumentTypeService } from 'src/app/services/rest/document-type.service' | import { DocumentTypeService } from 'src/app/services/rest/document-type.service' | ||||||
| import { TagService } from 'src/app/services/rest/tag.service' | import { TagService } from 'src/app/services/rest/tag.service' | ||||||
| import { CorrespondentService } from 'src/app/services/rest/correspondent.service' | import { CorrespondentService } from 'src/app/services/rest/correspondent.service' | ||||||
| import { FilterRule } from 'src/app/data/filter-rule' | import { filterRulesDiffer, FilterRule } from 'src/app/data/filter-rule' | ||||||
| import { | import { | ||||||
|   FILTER_ADDED_AFTER, |   FILTER_ADDED_AFTER, | ||||||
|   FILTER_ADDED_BEFORE, |   FILTER_ADDED_BEFORE, | ||||||
| @@ -204,7 +204,10 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | |||||||
|   @Input() |   @Input() | ||||||
|   set unmodifiedFilterRules(value: FilterRule[]) { |   set unmodifiedFilterRules(value: FilterRule[]) { | ||||||
|     this._unmodifiedFilterRules = value |     this._unmodifiedFilterRules = value | ||||||
|     this.checkIfRulesHaveChanged() |     this.rulesModified = filterRulesDiffer( | ||||||
|  |       this._unmodifiedFilterRules, | ||||||
|  |       this._filterRules | ||||||
|  |     ) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get unmodifiedFilterRules(): FilterRule[] { |   get unmodifiedFilterRules(): FilterRule[] { | ||||||
| @@ -313,7 +316,10 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | |||||||
|           break |           break | ||||||
|         case FILTER_ASN_ISNULL: |         case FILTER_ASN_ISNULL: | ||||||
|           this.textFilterTarget = TEXT_FILTER_TARGET_ASN |           this.textFilterTarget = TEXT_FILTER_TARGET_ASN | ||||||
|           this.textFilterModifier = TEXT_FILTER_MODIFIER_NULL |           this.textFilterModifier = | ||||||
|  |             rule.value == 'true' || rule.value == '1' | ||||||
|  |               ? TEXT_FILTER_MODIFIER_NULL | ||||||
|  |               : TEXT_FILTER_MODIFIER_NOTNULL | ||||||
|           break |           break | ||||||
|         case FILTER_ASN_GT: |         case FILTER_ASN_GT: | ||||||
|           this.textFilterTarget = TEXT_FILTER_TARGET_ASN |           this.textFilterTarget = TEXT_FILTER_TARGET_ASN | ||||||
| @@ -327,7 +333,10 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | |||||||
|           break |           break | ||||||
|       } |       } | ||||||
|     }) |     }) | ||||||
|     this.checkIfRulesHaveChanged() |     this.rulesModified = filterRulesDiffer( | ||||||
|  |       this._unmodifiedFilterRules, | ||||||
|  |       this._filterRules | ||||||
|  |     ) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get filterRules(): FilterRule[] { |   get filterRules(): FilterRule[] { | ||||||
| @@ -470,31 +479,6 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | |||||||
|  |  | ||||||
|   rulesModified: boolean = false |   rulesModified: boolean = false | ||||||
|  |  | ||||||
|   private checkIfRulesHaveChanged() { |  | ||||||
|     let modified = false |  | ||||||
|     if (this._unmodifiedFilterRules.length != this._filterRules.length) { |  | ||||||
|       modified = true |  | ||||||
|     } else { |  | ||||||
|       modified = this._unmodifiedFilterRules.some((rule) => { |  | ||||||
|         return ( |  | ||||||
|           this._filterRules.find( |  | ||||||
|             (fri) => fri.rule_type == rule.rule_type && fri.value == rule.value |  | ||||||
|           ) == undefined |  | ||||||
|         ) |  | ||||||
|       }) |  | ||||||
|  |  | ||||||
|       if (!modified) { |  | ||||||
|         // only check other direction if we havent already determined is modified |  | ||||||
|         modified = this._filterRules.some((rule) => { |  | ||||||
|           this._unmodifiedFilterRules.find( |  | ||||||
|             (fr) => fr.rule_type == rule.rule_type && fr.value == rule.value |  | ||||||
|           ) == undefined |  | ||||||
|         }) |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     this.rulesModified = modified |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   updateRules() { |   updateRules() { | ||||||
|     this.filterRulesChange.next(this.filterRules) |     this.filterRulesChange.next(this.filterRules) | ||||||
|   } |   } | ||||||
| @@ -547,29 +531,20 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | |||||||
|     this.updateRules() |     this.updateRules() | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   addTag(tagId: number) { |   toggleTag(tagId: number) { | ||||||
|     this.tagSelectionModel.set(tagId, ToggleableItemState.Selected) |     this.tagSelectionModel.toggle(tagId) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   addCorrespondent(correspondentId: number) { |   toggleCorrespondent(correspondentId: number) { | ||||||
|     this.correspondentSelectionModel.set( |     this.correspondentSelectionModel.toggle(correspondentId) | ||||||
|       correspondentId, |  | ||||||
|       ToggleableItemState.Selected |  | ||||||
|     ) |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   addDocumentType(documentTypeId: number) { |   toggleDocumentType(documentTypeId: number) { | ||||||
|     this.documentTypeSelectionModel.set( |     this.documentTypeSelectionModel.toggle(documentTypeId) | ||||||
|       documentTypeId, |  | ||||||
|       ToggleableItemState.Selected |  | ||||||
|     ) |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   addStoragePath(storagePathID: number) { |   toggleStoragePath(storagePathID: number) { | ||||||
|     this.storagePathSelectionModel.set( |     this.storagePathSelectionModel.toggle(storagePathID) | ||||||
|       storagePathID, |  | ||||||
|       ToggleableItemState.Selected |  | ||||||
|     ) |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   onTagsDropdownOpen() { |   onTagsDropdownOpen() { | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | |||||||
| import { FILTER_CORRESPONDENT } from 'src/app/data/filter-rule-type' | import { FILTER_CORRESPONDENT } from 'src/app/data/filter-rule-type' | ||||||
| import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent' | import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent' | ||||||
| import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' | import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' | ||||||
| import { QueryParamsService } from 'src/app/services/query-params.service' | import { DocumentListViewService } from 'src/app/services/document-list-view.service' | ||||||
| import { CorrespondentService } from 'src/app/services/rest/correspondent.service' | import { CorrespondentService } from 'src/app/services/rest/correspondent.service' | ||||||
| import { ToastService } from 'src/app/services/toast.service' | import { 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' | ||||||
| @@ -20,7 +20,7 @@ export class CorrespondentListComponent extends ManagementListComponent<Paperles | |||||||
|     correspondentsService: CorrespondentService, |     correspondentsService: CorrespondentService, | ||||||
|     modalService: NgbModal, |     modalService: NgbModal, | ||||||
|     toastService: ToastService, |     toastService: ToastService, | ||||||
|     queryParamsService: QueryParamsService, |     documentListViewService: DocumentListViewService, | ||||||
|     private datePipe: CustomDatePipe |     private datePipe: CustomDatePipe | ||||||
|   ) { |   ) { | ||||||
|     super( |     super( | ||||||
| @@ -28,7 +28,7 @@ export class CorrespondentListComponent extends ManagementListComponent<Paperles | |||||||
|       modalService, |       modalService, | ||||||
|       CorrespondentEditDialogComponent, |       CorrespondentEditDialogComponent, | ||||||
|       toastService, |       toastService, | ||||||
|       queryParamsService, |       documentListViewService, | ||||||
|       FILTER_CORRESPONDENT, |       FILTER_CORRESPONDENT, | ||||||
|       $localize`correspondent`, |       $localize`correspondent`, | ||||||
|       $localize`correspondents`, |       $localize`correspondents`, | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ import { Component } from '@angular/core' | |||||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||||||
| import { FILTER_DOCUMENT_TYPE } from 'src/app/data/filter-rule-type' | import { FILTER_DOCUMENT_TYPE } from 'src/app/data/filter-rule-type' | ||||||
| import { PaperlessDocumentType } from 'src/app/data/paperless-document-type' | import { PaperlessDocumentType } from 'src/app/data/paperless-document-type' | ||||||
| import { QueryParamsService } from 'src/app/services/query-params.service' | import { DocumentListViewService } from 'src/app/services/document-list-view.service' | ||||||
| import { DocumentTypeService } from 'src/app/services/rest/document-type.service' | import { DocumentTypeService } from 'src/app/services/rest/document-type.service' | ||||||
| import { ToastService } from 'src/app/services/toast.service' | import { 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,14 +18,14 @@ export class DocumentTypeListComponent extends ManagementListComponent<Paperless | |||||||
|     documentTypeService: DocumentTypeService, |     documentTypeService: DocumentTypeService, | ||||||
|     modalService: NgbModal, |     modalService: NgbModal, | ||||||
|     toastService: ToastService, |     toastService: ToastService, | ||||||
|     queryParamsService: QueryParamsService |     documentListViewService: DocumentListViewService | ||||||
|   ) { |   ) { | ||||||
|     super( |     super( | ||||||
|       documentTypeService, |       documentTypeService, | ||||||
|       modalService, |       modalService, | ||||||
|       DocumentTypeEditDialogComponent, |       DocumentTypeEditDialogComponent, | ||||||
|       toastService, |       toastService, | ||||||
|       queryParamsService, |       documentListViewService, | ||||||
|       FILTER_DOCUMENT_TYPE, |       FILTER_DOCUMENT_TYPE, | ||||||
|       $localize`document type`, |       $localize`document type`, | ||||||
|       $localize`document types`, |       $localize`document types`, | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ import { | |||||||
|   SortableDirective, |   SortableDirective, | ||||||
|   SortEvent, |   SortEvent, | ||||||
| } from 'src/app/directives/sortable.directive' | } from 'src/app/directives/sortable.directive' | ||||||
| import { QueryParamsService } from 'src/app/services/query-params.service' | import { DocumentListViewService } from 'src/app/services/document-list-view.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' | ||||||
| @@ -42,7 +42,7 @@ export abstract class ManagementListComponent<T extends ObjectWithId> | |||||||
|     private modalService: NgbModal, |     private modalService: NgbModal, | ||||||
|     private editDialogComponent: any, |     private editDialogComponent: any, | ||||||
|     private toastService: ToastService, |     private toastService: ToastService, | ||||||
|     private queryParamsService: QueryParamsService, |     private documentListViewService: DocumentListViewService, | ||||||
|     protected filterRuleType: number, |     protected filterRuleType: number, | ||||||
|     public typeName: string, |     public typeName: string, | ||||||
|     public typeNamePlural: string, |     public typeNamePlural: string, | ||||||
| @@ -141,7 +141,7 @@ export abstract class ManagementListComponent<T extends ObjectWithId> | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   filterDocuments(object: ObjectWithId) { |   filterDocuments(object: ObjectWithId) { | ||||||
|     this.queryParamsService.navigateWithFilterRules([ |     this.documentListViewService.quickFilter([ | ||||||
|       { rule_type: this.filterRuleType, value: object.id.toString() }, |       { rule_type: this.filterRuleType, value: object.id.toString() }, | ||||||
|     ]) |     ]) | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -22,7 +22,7 @@ | |||||||
|               <option *ngFor="let lang of displayLanguageOptions" [ngValue]="lang.code">{{lang.name}}<span *ngIf="lang.code && currentLocale != 'en-US'"> - {{lang.englishName}}</span></option> |               <option *ngFor="let lang of displayLanguageOptions" [ngValue]="lang.code">{{lang.name}}<span *ngIf="lang.code && currentLocale != 'en-US'"> - {{lang.englishName}}</span></option> | ||||||
|             </select> |             </select> | ||||||
|  |  | ||||||
|             <small class="form-text text-muted" i18n>You need to reload the page after applying a new language.</small> |             <small *ngIf="displayLanguageIsDirty" class="form-text text-primary" i18n>You need to reload the page after applying a new language.</small> | ||||||
|  |  | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ import { | |||||||
|   LanguageOption, |   LanguageOption, | ||||||
|   SettingsService, |   SettingsService, | ||||||
| } from 'src/app/services/settings.service' | } from 'src/app/services/settings.service' | ||||||
| import { ToastService } from 'src/app/services/toast.service' | import { Toast, ToastService } from 'src/app/services/toast.service' | ||||||
| import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms' | import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms' | ||||||
| import { Observable, Subscription, BehaviorSubject, first } from 'rxjs' | import { Observable, Subscription, BehaviorSubject, first } from 'rxjs' | ||||||
| import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings' | import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings' | ||||||
| @@ -61,6 +61,13 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent { | |||||||
|     ) |     ) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   get displayLanguageIsDirty(): boolean { | ||||||
|  |     return ( | ||||||
|  |       this.settingsForm.get('displayLanguage').value != | ||||||
|  |       this.store?.getValue()['displayLanguage'] | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  |  | ||||||
|   constructor( |   constructor( | ||||||
|     public savedViewService: SavedViewService, |     public savedViewService: SavedViewService, | ||||||
|     private documentListViewService: DocumentListViewService, |     private documentListViewService: DocumentListViewService, | ||||||
| @@ -170,6 +177,7 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private saveLocalSettings() { |   private saveLocalSettings() { | ||||||
|  |     const reloadRequired = this.displayLanguageIsDirty // just this one, for now | ||||||
|     this.settings.set( |     this.settings.set( | ||||||
|       SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE, |       SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE, | ||||||
|       this.settingsForm.value.bulkEditApplyOnClose |       this.settingsForm.value.bulkEditApplyOnClose | ||||||
| @@ -235,7 +243,20 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent { | |||||||
|           this.store.next(this.settingsForm.value) |           this.store.next(this.settingsForm.value) | ||||||
|           this.documentListViewService.updatePageSize() |           this.documentListViewService.updatePageSize() | ||||||
|           this.settings.updateAppearanceSettings() |           this.settings.updateAppearanceSettings() | ||||||
|           this.toastService.showInfo($localize`Settings saved successfully.`) |           let savedToast: Toast = { | ||||||
|  |             title: $localize`Settings saved`, | ||||||
|  |             content: $localize`Settings were saved successfully.`, | ||||||
|  |             delay: 500000, | ||||||
|  |           } | ||||||
|  |           if (reloadRequired) { | ||||||
|  |             ;(savedToast.content = $localize`Settings were saved successfully. Reload is required to apply some changes.`), | ||||||
|  |               (savedToast.actionName = $localize`Reload now`) | ||||||
|  |             savedToast.action = () => { | ||||||
|  |               location.reload() | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           this.toastService.show(savedToast) | ||||||
|         }, |         }, | ||||||
|         error: (error) => { |         error: (error) => { | ||||||
|           this.toastService.showError( |           this.toastService.showError( | ||||||
|   | |||||||
| @@ -3,7 +3,6 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | |||||||
| import { FILTER_STORAGE_PATH } from 'src/app/data/filter-rule-type' | import { FILTER_STORAGE_PATH } from 'src/app/data/filter-rule-type' | ||||||
| import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path' | import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path' | ||||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service' | import { DocumentListViewService } from 'src/app/services/document-list-view.service' | ||||||
| import { QueryParamsService } from 'src/app/services/query-params.service' |  | ||||||
| import { StoragePathService } from 'src/app/services/rest/storage-path.service' | import { StoragePathService } from 'src/app/services/rest/storage-path.service' | ||||||
| import { ToastService } from 'src/app/services/toast.service' | import { ToastService } from 'src/app/services/toast.service' | ||||||
| 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' | ||||||
| @@ -19,14 +18,14 @@ export class StoragePathListComponent extends ManagementListComponent<PaperlessS | |||||||
|     directoryService: StoragePathService, |     directoryService: StoragePathService, | ||||||
|     modalService: NgbModal, |     modalService: NgbModal, | ||||||
|     toastService: ToastService, |     toastService: ToastService, | ||||||
|     queryParamsService: QueryParamsService |     documentListViewService: DocumentListViewService | ||||||
|   ) { |   ) { | ||||||
|     super( |     super( | ||||||
|       directoryService, |       directoryService, | ||||||
|       modalService, |       modalService, | ||||||
|       StoragePathEditDialogComponent, |       StoragePathEditDialogComponent, | ||||||
|       toastService, |       toastService, | ||||||
|       queryParamsService, |       documentListViewService, | ||||||
|       FILTER_STORAGE_PATH, |       FILTER_STORAGE_PATH, | ||||||
|       $localize`storage path`, |       $localize`storage path`, | ||||||
|       $localize`storage paths`, |       $localize`storage paths`, | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ import { Component } from '@angular/core' | |||||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||||||
| 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 { PaperlessTag } from 'src/app/data/paperless-tag' | import { PaperlessTag } from 'src/app/data/paperless-tag' | ||||||
| import { QueryParamsService } from 'src/app/services/query-params.service' | import { DocumentListViewService } from 'src/app/services/document-list-view.service' | ||||||
| import { TagService } from 'src/app/services/rest/tag.service' | import { TagService } from 'src/app/services/rest/tag.service' | ||||||
| import { ToastService } from 'src/app/services/toast.service' | import { ToastService } from 'src/app/services/toast.service' | ||||||
| import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component' | import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component' | ||||||
| @@ -18,14 +18,14 @@ export class TagListComponent extends ManagementListComponent<PaperlessTag> { | |||||||
|     tagService: TagService, |     tagService: TagService, | ||||||
|     modalService: NgbModal, |     modalService: NgbModal, | ||||||
|     toastService: ToastService, |     toastService: ToastService, | ||||||
|     queryParamsService: QueryParamsService |     documentListViewService: DocumentListViewService | ||||||
|   ) { |   ) { | ||||||
|     super( |     super( | ||||||
|       tagService, |       tagService, | ||||||
|       modalService, |       modalService, | ||||||
|       TagEditDialogComponent, |       TagEditDialogComponent, | ||||||
|       toastService, |       toastService, | ||||||
|       queryParamsService, |       documentListViewService, | ||||||
|       FILTER_HAS_TAGS_ALL, |       FILTER_HAS_TAGS_ALL, | ||||||
|       $localize`tag`, |       $localize`tag`, | ||||||
|       $localize`tags`, |       $localize`tags`, | ||||||
|   | |||||||
							
								
								
									
										120
									
								
								src-ui/src/app/components/manage/tasks/tasks.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,120 @@ | |||||||
|  | <app-page-header title="File Tasks" i18n-title> | ||||||
|  |   <div class="btn-toolbar col col-md-auto"> | ||||||
|  |     <button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size == 0"> | ||||||
|  |       <svg class="sidebaricon" fill="currentColor"> | ||||||
|  |         <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||||
|  |       </svg> <ng-container i18n>Clear selection</ng-container> | ||||||
|  |     </button> | ||||||
|  |     <button class="btn btn-sm btn-outline-primary me-4" (click)="dismissTasks()" [disabled]="tasksService.total == 0"> | ||||||
|  |       <svg class="sidebaricon" fill="currentColor"> | ||||||
|  |         <use xlink:href="assets/bootstrap-icons.svg#check2-all"/> | ||||||
|  |       </svg> <ng-container i18n>{{dismissButtonText}}</ng-container> | ||||||
|  |     </button> | ||||||
|  |     <button class="btn btn-sm btn-outline-primary" (click)="tasksService.reload()"> | ||||||
|  |       <svg *ngIf="!tasksService.loading" class="sidebaricon" fill="currentColor"> | ||||||
|  |         <use xlink:href="assets/bootstrap-icons.svg#arrow-clockwise"/> | ||||||
|  |       </svg> | ||||||
|  |       <ng-container *ngIf="tasksService.loading"> | ||||||
|  |         <div class="spinner-border spinner-border-sm fw-normal" role="status"></div> | ||||||
|  |         <div class="visually-hidden" i18n>Loading...</div> | ||||||
|  |       </ng-container> <ng-container i18n>Refresh</ng-container> | ||||||
|  |     </button> | ||||||
|  |   </div> | ||||||
|  | </app-page-header> | ||||||
|  |  | ||||||
|  | <ng-container *ngIf="!tasksService.completedFileTasks && tasksService.loading"> | ||||||
|  |   <div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div> | ||||||
|  |   <div class="visually-hidden" i18n>Loading...</div> | ||||||
|  | </ng-container> | ||||||
|  |  | ||||||
|  | <ng-template let-tasks="tasks" #tasksTemplate> | ||||||
|  |   <table class="table table-striped align-middle border shadow-sm"> | ||||||
|  |     <thead> | ||||||
|  |       <tr> | ||||||
|  |         <th scope="col"> | ||||||
|  |           <div class="form-check"> | ||||||
|  |             <input type="checkbox" class="form-check-input" id="all-tasks" [disabled]="currentTasks.length == 0" (click)="toggleAll($event); $event.stopPropagation();"> | ||||||
|  |             <label class="form-check-label" for="all-tasks"></label> | ||||||
|  |           </div> | ||||||
|  |         </th> | ||||||
|  |         <th scope="col" i18n>Name</th> | ||||||
|  |         <th scope="col" class="d-none d-lg-table-cell" i18n>Created</th> | ||||||
|  |         <th scope="col" class="d-none d-lg-table-cell" *ngIf="activeTab != 'started' && activeTab != 'queued'" i18n>Results</th> | ||||||
|  |         <th scope="col" class="d-table-cell d-lg-none" i18n>Info</th> | ||||||
|  |         <th scope="col" i18n>Actions</th> | ||||||
|  |       </tr> | ||||||
|  |     </thead> | ||||||
|  |     <tbody> | ||||||
|  |       <ng-container *ngFor="let task of tasks"> | ||||||
|  |       <tr (click)="toggleSelected(task, $event); $event.stopPropagation();"> | ||||||
|  |         <th> | ||||||
|  |           <div class="form-check"> | ||||||
|  |             <input type="checkbox" class="form-check-input" id="task{{task.id}}" [checked]="selectedTasks.has(task.id)" (click)="toggleSelected(task, $event); $event.stopPropagation();"> | ||||||
|  |             <label class="form-check-label" for="task{{task.id}}"></label> | ||||||
|  |           </div> | ||||||
|  |         </th> | ||||||
|  |         <td class="overflow-auto">{{ task.name }}</td> | ||||||
|  |         <td class="d-none d-lg-table-cell">{{ task.created | customDate:'short' }}</td> | ||||||
|  |         <td class="d-none d-lg-table-cell" *ngIf="activeTab != 'incomplete'"> | ||||||
|  |           <div *ngIf="task.result.length > 50" class="result" (click)="expandTask(task); $event.stopPropagation();" | ||||||
|  |             [ngbPopover]="resultPopover" popoverClass="shadow small mobile" triggers="mouseenter:mouseleave" container="body"> | ||||||
|  |             <span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result | slice:0:50 }}…</span> | ||||||
|  |           </div> | ||||||
|  |           <span *ngIf="task.result.length <= 50" class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result }}</span> | ||||||
|  |           <ng-template #resultPopover> | ||||||
|  |             <pre class="small mb-0">{{ task.result | slice:0:300 }}<ng-container *ngIf="task.result.length > 300">…</ng-container></pre> | ||||||
|  |             <ng-container *ngIf="task.result.length > 300"><br/><em>(<ng-container i18n>click for full output</ng-container>)</em></ng-container> | ||||||
|  |           </ng-template> | ||||||
|  |         </td> | ||||||
|  |         <td class="d-lg-none"> | ||||||
|  |           <button class="btn btn-link" (click)="expandTask(task); $event.stopPropagation();"> | ||||||
|  |             <svg fill="currentColor" class="" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16"> | ||||||
|  |               <use xlink:href="assets/bootstrap-icons.svg#info-circle" /> | ||||||
|  |             </svg> | ||||||
|  |           </button> | ||||||
|  |         </td> | ||||||
|  |         <td scope="row"> | ||||||
|  |           <button class="btn btn-sm btn-outline-secondary" (click)="dismissTask(task); $event.stopPropagation();"> | ||||||
|  |             <svg class="sidebaricon" fill="currentColor"> | ||||||
|  |               <use xlink:href="assets/bootstrap-icons.svg#check"/> | ||||||
|  |             </svg> <ng-container i18n>Dismiss</ng-container> | ||||||
|  |           </button> | ||||||
|  |         </td> | ||||||
|  |       </tr> | ||||||
|  |       <tr> | ||||||
|  |         <td class="p-0" [class.border-0]="expandedTask != task.id" colspan="5"> | ||||||
|  |           <pre #collapse="ngbCollapse" [ngbCollapse]="expandedTask !== task.id" class="small mb-0"><div class="small p-1 p-lg-3 ms-lg-3">{{ task.result }}</div></pre> | ||||||
|  |         </td> | ||||||
|  |       </tr> | ||||||
|  |       </ng-container> | ||||||
|  |     </tbody> | ||||||
|  |   </table> | ||||||
|  | </ng-template> | ||||||
|  |  | ||||||
|  | <ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs"> | ||||||
|  |   <li ngbNavItem="failed"> | ||||||
|  |     <a ngbNavLink i18n>Failed <span *ngIf="tasksService.failedFileTasks.length > 0" class="badge bg-danger ms-1">{{tasksService.failedFileTasks.length}}</span></a> | ||||||
|  |     <ng-template ngbNavContent> | ||||||
|  |       <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.failedFileTasks}"></ng-container> | ||||||
|  |     </ng-template> | ||||||
|  |   </li> | ||||||
|  |   <li ngbNavItem="completed"> | ||||||
|  |     <a ngbNavLink i18n>Complete <span *ngIf="tasksService.completedFileTasks.length > 0" class="badge bg-secondary ms-1">{{tasksService.completedFileTasks.length}}</span></a> | ||||||
|  |     <ng-template ngbNavContent> | ||||||
|  |       <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.completedFileTasks}"></ng-container> | ||||||
|  |     </ng-template> | ||||||
|  |   </li> | ||||||
|  |   <li ngbNavItem="started"> | ||||||
|  |     <a ngbNavLink i18n>Started <span *ngIf="tasksService.startedFileTasks.length > 0" class="badge bg-secondary ms-1">{{tasksService.startedFileTasks.length}}</span></a> | ||||||
|  |     <ng-template ngbNavContent> | ||||||
|  |       <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.startedFileTasks}"></ng-container> | ||||||
|  |     </ng-template> | ||||||
|  |   </li> | ||||||
|  |   <li ngbNavItem="queued"> | ||||||
|  |     <a ngbNavLink i18n>Queued <span *ngIf="tasksService.queuedFileTasks.length > 0" class="badge bg-secondary ms-1">{{tasksService.queuedFileTasks.length}}</span></a> | ||||||
|  |     <ng-template ngbNavContent> | ||||||
|  |       <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.queuedFileTasks}"></ng-container> | ||||||
|  |     </ng-template> | ||||||
|  |   </li> | ||||||
|  | </ul> | ||||||
|  | <div [ngbNavOutlet]="nav"></div> | ||||||
							
								
								
									
										22
									
								
								src-ui/src/app/components/manage/tasks/tasks.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,22 @@ | |||||||
|  | ::ng-deep .popover { | ||||||
|  |     max-width: 350px; | ||||||
|  |  | ||||||
|  |     pre { | ||||||
|  |         white-space: pre-wrap; | ||||||
|  |         word-break: break-word; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pre { | ||||||
|  |     white-space: pre-wrap; | ||||||
|  |     word-break: break-word; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .result { | ||||||
|  |     cursor: pointer; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .btn .spinner-border-sm { | ||||||
|  |     width: 0.8rem; | ||||||
|  |     height: 0.8rem; | ||||||
|  | } | ||||||
							
								
								
									
										109
									
								
								src-ui/src/app/components/manage/tasks/tasks.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,109 @@ | |||||||
|  | import { Component, OnInit, OnDestroy } from '@angular/core' | ||||||
|  | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||||||
|  | import { takeUntil, Subject, first } from 'rxjs' | ||||||
|  | import { PaperlessTask } from 'src/app/data/paperless-task' | ||||||
|  | import { TasksService } from 'src/app/services/tasks.service' | ||||||
|  | import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' | ||||||
|  |  | ||||||
|  | @Component({ | ||||||
|  |   selector: 'app-tasks', | ||||||
|  |   templateUrl: './tasks.component.html', | ||||||
|  |   styleUrls: ['./tasks.component.scss'], | ||||||
|  | }) | ||||||
|  | export class TasksComponent implements OnInit, OnDestroy { | ||||||
|  |   public activeTab: string | ||||||
|  |   public selectedTasks: Set<number> = new Set() | ||||||
|  |   private unsubscribeNotifer = new Subject() | ||||||
|  |   public expandedTask: number | ||||||
|  |  | ||||||
|  |   get dismissButtonText(): string { | ||||||
|  |     return this.selectedTasks.size > 0 | ||||||
|  |       ? $localize`Dismiss selected` | ||||||
|  |       : $localize`Dismiss all` | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   constructor( | ||||||
|  |     public tasksService: TasksService, | ||||||
|  |     private modalService: NgbModal | ||||||
|  |   ) {} | ||||||
|  |  | ||||||
|  |   ngOnInit() { | ||||||
|  |     this.tasksService.reload() | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ngOnDestroy() { | ||||||
|  |     this.unsubscribeNotifer.next(true) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   dismissTask(task: PaperlessTask) { | ||||||
|  |     this.dismissTasks(task) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   dismissTasks(task: PaperlessTask = undefined) { | ||||||
|  |     let tasks = task ? new Set([task.id]) : this.selectedTasks | ||||||
|  |     if (!task && this.selectedTasks.size == 0) | ||||||
|  |       tasks = new Set(this.tasksService.allFileTasks.map((t) => t.id)) | ||||||
|  |     if (tasks.size > 1) { | ||||||
|  |       let modal = this.modalService.open(ConfirmDialogComponent, { | ||||||
|  |         backdrop: 'static', | ||||||
|  |       }) | ||||||
|  |       modal.componentInstance.title = $localize`Confirm Dismiss All` | ||||||
|  |       modal.componentInstance.messageBold = | ||||||
|  |         $localize`Dismiss all` + ` ${tasks.size} ` + $localize`tasks?` | ||||||
|  |       modal.componentInstance.btnClass = 'btn-warning' | ||||||
|  |       modal.componentInstance.btnCaption = $localize`Dismiss` | ||||||
|  |       modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => { | ||||||
|  |         modal.componentInstance.buttonsEnabled = false | ||||||
|  |         modal.close() | ||||||
|  |         this.tasksService.dismissTasks(tasks) | ||||||
|  |         this.selectedTasks.clear() | ||||||
|  |       }) | ||||||
|  |     } else { | ||||||
|  |       this.tasksService.dismissTasks(tasks) | ||||||
|  |       this.selectedTasks.clear() | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   expandTask(task: PaperlessTask) { | ||||||
|  |     this.expandedTask = this.expandedTask == task.id ? undefined : task.id | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   toggleSelected(task: PaperlessTask) { | ||||||
|  |     this.selectedTasks.has(task.id) | ||||||
|  |       ? this.selectedTasks.delete(task.id) | ||||||
|  |       : this.selectedTasks.add(task.id) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get currentTasks(): PaperlessTask[] { | ||||||
|  |     let tasks: PaperlessTask[] | ||||||
|  |     switch (this.activeTab) { | ||||||
|  |       case 'queued': | ||||||
|  |         tasks = this.tasksService.queuedFileTasks | ||||||
|  |         break | ||||||
|  |       case 'started': | ||||||
|  |         tasks = this.tasksService.startedFileTasks | ||||||
|  |         break | ||||||
|  |       case 'completed': | ||||||
|  |         tasks = this.tasksService.completedFileTasks | ||||||
|  |         break | ||||||
|  |       case 'failed': | ||||||
|  |         tasks = this.tasksService.failedFileTasks | ||||||
|  |         break | ||||||
|  |       default: | ||||||
|  |         break | ||||||
|  |     } | ||||||
|  |     return tasks | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   toggleAll(event: PointerEvent) { | ||||||
|  |     if ((event.target as HTMLInputElement).checked) { | ||||||
|  |       this.selectedTasks = new Set(this.currentTasks.map((t) => t.id)) | ||||||
|  |     } else { | ||||||
|  |       this.clearSelection() | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   clearSelection() { | ||||||
|  |     this.selectedTasks.clear() | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,34 +1,38 @@ | |||||||
| export const FILTER_TITLE = 0 | export const FILTER_TITLE = 0 | ||||||
| export const FILTER_CONTENT = 1 | export const FILTER_CONTENT = 1 | ||||||
|  |  | ||||||
| export const FILTER_ASN = 2 | export const FILTER_ASN = 2 | ||||||
|  | export const FILTER_ASN_ISNULL = 18 | ||||||
|  | export const FILTER_ASN_GT = 23 | ||||||
|  | export const FILTER_ASN_LT = 24 | ||||||
|  |  | ||||||
| export const FILTER_CORRESPONDENT = 3 | export const FILTER_CORRESPONDENT = 3 | ||||||
|  |  | ||||||
| export const FILTER_DOCUMENT_TYPE = 4 | export const FILTER_DOCUMENT_TYPE = 4 | ||||||
|  |  | ||||||
| export const FILTER_IS_IN_INBOX = 5 | export const FILTER_IS_IN_INBOX = 5 | ||||||
| export const FILTER_HAS_TAGS_ALL = 6 | export const FILTER_HAS_TAGS_ALL = 6 | ||||||
| export const FILTER_HAS_ANY_TAG = 7 | export const FILTER_HAS_ANY_TAG = 7 | ||||||
|  | export const FILTER_DOES_NOT_HAVE_TAG = 17 | ||||||
| export const FILTER_HAS_TAGS_ANY = 22 | export const FILTER_HAS_TAGS_ANY = 22 | ||||||
|  |  | ||||||
|  | export const FILTER_STORAGE_PATH = 25 | ||||||
|  |  | ||||||
| export const FILTER_CREATED_BEFORE = 8 | export const FILTER_CREATED_BEFORE = 8 | ||||||
| export const FILTER_CREATED_AFTER = 9 | export const FILTER_CREATED_AFTER = 9 | ||||||
| export const FILTER_CREATED_YEAR = 10 | export const FILTER_CREATED_YEAR = 10 | ||||||
| export const FILTER_CREATED_MONTH = 11 | export const FILTER_CREATED_MONTH = 11 | ||||||
| export const FILTER_CREATED_DAY = 12 | export const FILTER_CREATED_DAY = 12 | ||||||
|  |  | ||||||
| export const FILTER_ADDED_BEFORE = 13 | export const FILTER_ADDED_BEFORE = 13 | ||||||
| export const FILTER_ADDED_AFTER = 14 | export const FILTER_ADDED_AFTER = 14 | ||||||
|  |  | ||||||
| export const FILTER_MODIFIED_BEFORE = 15 | export const FILTER_MODIFIED_BEFORE = 15 | ||||||
| export const FILTER_MODIFIED_AFTER = 16 | export const FILTER_MODIFIED_AFTER = 16 | ||||||
|  |  | ||||||
| export const FILTER_DOES_NOT_HAVE_TAG = 17 | export const FILTER_TITLE_CONTENT = 19 | ||||||
|  | export const FILTER_FULLTEXT_QUERY = 20 | ||||||
| export const FILTER_ASN_ISNULL = 18 | export const FILTER_FULLTEXT_MORELIKE = 21 | ||||||
| export const FILTER_ASN_GT = 19 |  | ||||||
| export const FILTER_ASN_LT = 20 |  | ||||||
|  |  | ||||||
| export const FILTER_TITLE_CONTENT = 21 |  | ||||||
|  |  | ||||||
| export const FILTER_FULLTEXT_QUERY = 22 |  | ||||||
| export const FILTER_FULLTEXT_MORELIKE = 23 |  | ||||||
|  |  | ||||||
| export const FILTER_STORAGE_PATH = 30 |  | ||||||
|  |  | ||||||
| export const FILTER_RULE_TYPES: FilterRuleType[] = [ | export const FILTER_RULE_TYPES: FilterRuleType[] = [ | ||||||
|   { |   { | ||||||
|   | |||||||
| @@ -25,6 +25,25 @@ export function isFullTextFilterRule(filterRules: FilterRule[]): boolean { | |||||||
|   ) |   ) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export function filterRulesDiffer( | ||||||
|  |   filterRulesA: FilterRule[], | ||||||
|  |   filterRulesB: FilterRule[] | ||||||
|  | ): boolean { | ||||||
|  |   let differ = false | ||||||
|  |   if (filterRulesA.length != filterRulesB.length) { | ||||||
|  |     differ = true | ||||||
|  |   } else { | ||||||
|  |     differ = filterRulesA.some((rule) => { | ||||||
|  |       return ( | ||||||
|  |         filterRulesB.find( | ||||||
|  |           (fri) => fri.rule_type == rule.rule_type && fri.value == rule.value | ||||||
|  |         ) == undefined | ||||||
|  |       ) | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |   return differ | ||||||
|  | } | ||||||
|  |  | ||||||
| export interface FilterRule { | export interface FilterRule { | ||||||
|   rule_type: number |   rule_type: number | ||||||
|   value: string |   value: string | ||||||
|   | |||||||
| @@ -37,8 +37,12 @@ export interface PaperlessDocument extends ObjectWithId { | |||||||
|  |  | ||||||
|   checksum?: string |   checksum?: string | ||||||
|  |  | ||||||
|  |   // UTC | ||||||
|   created?: Date |   created?: Date | ||||||
|  |  | ||||||
|  |   // localized date | ||||||
|  |   created_date?: Date | ||||||
|  |  | ||||||
|   modified?: Date |   modified?: Date | ||||||
|  |  | ||||||
|   added?: Date |   added?: Date | ||||||
|   | |||||||
							
								
								
									
										32
									
								
								src-ui/src/app/data/paperless-task.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,32 @@ | |||||||
|  | import { ObjectWithId } from './object-with-id' | ||||||
|  |  | ||||||
|  | export enum PaperlessTaskType { | ||||||
|  |   // just file tasks, for now | ||||||
|  |   File = 'file', | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export enum PaperlessTaskStatus { | ||||||
|  |   Queued = 'queued', | ||||||
|  |   Started = 'started', | ||||||
|  |   Complete = 'complete', | ||||||
|  |   Failed = 'failed', | ||||||
|  |   Unknown = 'unknown', | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface PaperlessTask extends ObjectWithId { | ||||||
|  |   type: PaperlessTaskType | ||||||
|  |  | ||||||
|  |   status: PaperlessTaskStatus | ||||||
|  |  | ||||||
|  |   acknowledged: boolean | ||||||
|  |  | ||||||
|  |   task_id: string | ||||||
|  |  | ||||||
|  |   name: string | ||||||
|  |  | ||||||
|  |   created: Date | ||||||
|  |  | ||||||
|  |   started?: Date | ||||||
|  |  | ||||||
|  |   result: string | ||||||
|  | } | ||||||
| @@ -2,7 +2,6 @@ import { DatePipe } from '@angular/common' | |||||||
| import { Inject, LOCALE_ID, Pipe, PipeTransform } from '@angular/core' | import { Inject, LOCALE_ID, Pipe, PipeTransform } from '@angular/core' | ||||||
| import { SETTINGS_KEYS } from '../data/paperless-uisettings' | import { SETTINGS_KEYS } from '../data/paperless-uisettings' | ||||||
| import { SettingsService } from '../services/settings.service' | import { SettingsService } from '../services/settings.service' | ||||||
| import { normalizeDateStr } from '../utils/date' |  | ||||||
|  |  | ||||||
| const FORMAT_TO_ISO_FORMAT = { | const FORMAT_TO_ISO_FORMAT = { | ||||||
|   longDate: 'y-MM-dd', |   longDate: 'y-MM-dd', | ||||||
| @@ -35,7 +34,6 @@ export class CustomDatePipe implements PipeTransform { | |||||||
|       this.settings.get(SETTINGS_KEYS.DATE_LOCALE) || |       this.settings.get(SETTINGS_KEYS.DATE_LOCALE) || | ||||||
|       this.defaultLocale |       this.defaultLocale | ||||||
|     let f = format || this.settings.get(SETTINGS_KEYS.DATE_FORMAT) |     let f = format || this.settings.get(SETTINGS_KEYS.DATE_FORMAT) | ||||||
|     if (typeof value == 'string') value = normalizeDateStr(value) |  | ||||||
|     if (l == 'iso-8601') { |     if (l == 'iso-8601') { | ||||||
|       return this.datePipe.transform(value, FORMAT_TO_ISO_FORMAT[f], timezone) |       return this.datePipe.transform(value, FORMAT_TO_ISO_FORMAT[f], timezone) | ||||||
|     } else { |     } else { | ||||||
|   | |||||||
| @@ -1,7 +1,8 @@ | |||||||
| import { Injectable } from '@angular/core' | import { Injectable } from '@angular/core' | ||||||
| import { ActivatedRoute, Params, Router } from '@angular/router' | import { ParamMap, Router } from '@angular/router' | ||||||
| import { Observable } from 'rxjs' | import { Observable } from 'rxjs' | ||||||
| import { | import { | ||||||
|  |   filterRulesDiffer, | ||||||
|   cloneFilterRules, |   cloneFilterRules, | ||||||
|   FilterRule, |   FilterRule, | ||||||
|   isFullTextFilterRule, |   isFullTextFilterRule, | ||||||
| @@ -10,13 +11,14 @@ import { PaperlessDocument } from '../data/paperless-document' | |||||||
| import { PaperlessSavedView } from '../data/paperless-saved-view' | import { PaperlessSavedView } from '../data/paperless-saved-view' | ||||||
| import { SETTINGS_KEYS } from '../data/paperless-uisettings' | import { SETTINGS_KEYS } from '../data/paperless-uisettings' | ||||||
| import { DOCUMENT_LIST_SERVICE } from '../data/storage-keys' | import { DOCUMENT_LIST_SERVICE } from '../data/storage-keys' | ||||||
|  | import { generateParams, parseParams } from '../utils/query-params' | ||||||
| import { DocumentService, DOCUMENT_SORT_FIELDS } from './rest/document.service' | import { DocumentService, DOCUMENT_SORT_FIELDS } from './rest/document.service' | ||||||
| import { SettingsService } from './settings.service' | import { SettingsService } from './settings.service' | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Captures the current state of the list view. |  * Captures the current state of the list view. | ||||||
|  */ |  */ | ||||||
| interface ListViewState { | export interface ListViewState { | ||||||
|   /** |   /** | ||||||
|    * Title of the document list view. Either "Documents" (localized) or the name of a saved view. |    * Title of the document list view. Either "Documents" (localized) or the name of a saved view. | ||||||
|    */ |    */ | ||||||
| @@ -32,7 +34,7 @@ interface ListViewState { | |||||||
|   /** |   /** | ||||||
|    * Total amount of documents with the current filter rules. Used to calculate the number of pages. |    * Total amount of documents with the current filter rules. Used to calculate the number of pages. | ||||||
|    */ |    */ | ||||||
|   collectionSize: number |   collectionSize?: number | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Currently selected sort field. |    * Currently selected sort field. | ||||||
| @@ -66,6 +68,7 @@ interface ListViewState { | |||||||
| }) | }) | ||||||
| export class DocumentListViewService { | export class DocumentListViewService { | ||||||
|   isReloading: boolean = false |   isReloading: boolean = false | ||||||
|  |   initialized: boolean = false | ||||||
|   error: string = null |   error: string = null | ||||||
|  |  | ||||||
|   rangeSelectionAnchorIndex: number |   rangeSelectionAnchorIndex: number | ||||||
| @@ -85,6 +88,32 @@ export class DocumentListViewService { | |||||||
|     return this.activeListViewState.title |     return this.activeListViewState.title | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   constructor( | ||||||
|  |     private documentService: DocumentService, | ||||||
|  |     private settings: SettingsService, | ||||||
|  |     private router: Router | ||||||
|  |   ) { | ||||||
|  |     let documentListViewConfigJson = localStorage.getItem( | ||||||
|  |       DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG | ||||||
|  |     ) | ||||||
|  |     if (documentListViewConfigJson) { | ||||||
|  |       try { | ||||||
|  |         let savedState: ListViewState = JSON.parse(documentListViewConfigJson) | ||||||
|  |         // Remove null elements from the restored state | ||||||
|  |         Object.keys(savedState).forEach((k) => { | ||||||
|  |           if (savedState[k] == null) { | ||||||
|  |             delete savedState[k] | ||||||
|  |           } | ||||||
|  |         }) | ||||||
|  |         //only use restored state attributes instead of defaults if they are not null | ||||||
|  |         let newState = Object.assign(this.defaultListViewState(), savedState) | ||||||
|  |         this.listViewStates.set(null, newState) | ||||||
|  |       } catch (e) { | ||||||
|  |         localStorage.removeItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private defaultListViewState(): ListViewState { |   private defaultListViewState(): ListViewState { | ||||||
|     return { |     return { | ||||||
|       title: null, |       title: null, | ||||||
| @@ -122,20 +151,53 @@ export class DocumentListViewService { | |||||||
|     if (closeCurrentView) { |     if (closeCurrentView) { | ||||||
|       this._activeSavedViewId = null |       this._activeSavedViewId = null | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     this.activeListViewState.filterRules = cloneFilterRules(view.filter_rules) |     this.activeListViewState.filterRules = cloneFilterRules(view.filter_rules) | ||||||
|     this.activeListViewState.sortField = view.sort_field |     this.activeListViewState.sortField = view.sort_field | ||||||
|     this.activeListViewState.sortReverse = view.sort_reverse |     this.activeListViewState.sortReverse = view.sort_reverse | ||||||
|     if (this._activeSavedViewId) { |     if (this._activeSavedViewId) { | ||||||
|       this.activeListViewState.title = view.name |       this.activeListViewState.title = view.name | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     this.reduceSelectionToFilter() |     this.reduceSelectionToFilter() | ||||||
|  |  | ||||||
|  |     if (!this.router.routerState.snapshot.url.includes('/view/')) { | ||||||
|  |       this.router.navigate([], { | ||||||
|  |         queryParams: { view: view.id }, | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   reload(onFinish?) { |   loadFromQueryParams(queryParams: ParamMap) { | ||||||
|  |     const paramsEmpty: boolean = queryParams.keys.length == 0 | ||||||
|  |     let newState: ListViewState = this.listViewStates.get(null) | ||||||
|  |     if (!paramsEmpty) newState = parseParams(queryParams) | ||||||
|  |     if (newState == undefined) newState = this.defaultListViewState() // if nothing in local storage | ||||||
|  |  | ||||||
|  |     // only reload if things have changed | ||||||
|  |     if ( | ||||||
|  |       !this.initialized || | ||||||
|  |       paramsEmpty || | ||||||
|  |       this.activeListViewState.sortField !== newState.sortField || | ||||||
|  |       this.activeListViewState.sortReverse !== newState.sortReverse || | ||||||
|  |       this.activeListViewState.currentPage !== newState.currentPage || | ||||||
|  |       filterRulesDiffer( | ||||||
|  |         this.activeListViewState.filterRules, | ||||||
|  |         newState.filterRules | ||||||
|  |       ) | ||||||
|  |     ) { | ||||||
|  |       this.activeListViewState.filterRules = newState.filterRules | ||||||
|  |       this.activeListViewState.sortField = newState.sortField | ||||||
|  |       this.activeListViewState.sortReverse = newState.sortReverse | ||||||
|  |       this.activeListViewState.currentPage = newState.currentPage | ||||||
|  |       this.reload(null, paramsEmpty) // update the params if there arent any | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   reload(onFinish?, updateQueryParams: boolean = true) { | ||||||
|     this.isReloading = true |     this.isReloading = true | ||||||
|     this.error = null |     this.error = null | ||||||
|     let activeListViewState = this.activeListViewState |     let activeListViewState = this.activeListViewState | ||||||
|  |  | ||||||
|     this.documentService |     this.documentService | ||||||
|       .listFiltered( |       .listFiltered( | ||||||
|         activeListViewState.currentPage, |         activeListViewState.currentPage, | ||||||
| @@ -146,9 +208,18 @@ export class DocumentListViewService { | |||||||
|       ) |       ) | ||||||
|       .subscribe({ |       .subscribe({ | ||||||
|         next: (result) => { |         next: (result) => { | ||||||
|  |           this.initialized = true | ||||||
|           this.isReloading = false |           this.isReloading = false | ||||||
|           activeListViewState.collectionSize = result.count |           activeListViewState.collectionSize = result.count | ||||||
|           activeListViewState.documents = result.results |           activeListViewState.documents = result.results | ||||||
|  |  | ||||||
|  |           if (updateQueryParams && !this._activeSavedViewId) { | ||||||
|  |             let base = ['/documents'] | ||||||
|  |             this.router.navigate(base, { | ||||||
|  |               queryParams: generateParams(activeListViewState), | ||||||
|  |             }) | ||||||
|  |           } | ||||||
|  |  | ||||||
|           if (onFinish) { |           if (onFinish) { | ||||||
|             onFinish() |             onFinish() | ||||||
|           } |           } | ||||||
| @@ -191,6 +262,7 @@ export class DocumentListViewService { | |||||||
|     ) { |     ) { | ||||||
|       this.activeListViewState.sortField = 'created' |       this.activeListViewState.sortField = 'created' | ||||||
|     } |     } | ||||||
|  |     this._activeSavedViewId = null | ||||||
|     this.activeListViewState.filterRules = filterRules |     this.activeListViewState.filterRules = filterRules | ||||||
|     this.reload() |     this.reload() | ||||||
|     this.reduceSelectionToFilter() |     this.reduceSelectionToFilter() | ||||||
| @@ -202,6 +274,7 @@ export class DocumentListViewService { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   set sortField(field: string) { |   set sortField(field: string) { | ||||||
|  |     this._activeSavedViewId = null | ||||||
|     this.activeListViewState.sortField = field |     this.activeListViewState.sortField = field | ||||||
|     this.reload() |     this.reload() | ||||||
|     this.saveDocumentListView() |     this.saveDocumentListView() | ||||||
| @@ -212,6 +285,7 @@ export class DocumentListViewService { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   set sortReverse(reverse: boolean) { |   set sortReverse(reverse: boolean) { | ||||||
|  |     this._activeSavedViewId = null | ||||||
|     this.activeListViewState.sortReverse = reverse |     this.activeListViewState.sortReverse = reverse | ||||||
|     this.reload() |     this.reload() | ||||||
|     this.saveDocumentListView() |     this.saveDocumentListView() | ||||||
| @@ -221,13 +295,6 @@ export class DocumentListViewService { | |||||||
|     return this.activeListViewState.sortReverse |     return this.activeListViewState.sortReverse | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get sortParams(): Params { |  | ||||||
|     return { |  | ||||||
|       sortField: this.sortField, |  | ||||||
|       sortReverse: this.sortReverse, |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   get collectionSize(): number { |   get collectionSize(): number { | ||||||
|     return this.activeListViewState.collectionSize |     return this.activeListViewState.collectionSize | ||||||
|   } |   } | ||||||
| @@ -237,6 +304,8 @@ export class DocumentListViewService { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   set currentPage(page: number) { |   set currentPage(page: number) { | ||||||
|  |     if (this.activeListViewState.currentPage == page) return | ||||||
|  |     this._activeSavedViewId = null | ||||||
|     this.activeListViewState.currentPage = page |     this.activeListViewState.currentPage = page | ||||||
|     this.reload() |     this.reload() | ||||||
|     this.saveDocumentListView() |     this.saveDocumentListView() | ||||||
| @@ -273,6 +342,10 @@ export class DocumentListViewService { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   quickFilter(filterRules: FilterRule[]) { | ||||||
|  |     this.filterRules = filterRules | ||||||
|  |   } | ||||||
|  |  | ||||||
|   getLastPage(): number { |   getLastPage(): number { | ||||||
|     return Math.ceil(this.collectionSize / this.currentPageSize) |     return Math.ceil(this.collectionSize / this.currentPageSize) | ||||||
|   } |   } | ||||||
| @@ -431,29 +504,4 @@ export class DocumentListViewService { | |||||||
|   documentIndexInCurrentView(documentID: number): number { |   documentIndexInCurrentView(documentID: number): number { | ||||||
|     return this.documents.map((d) => d.id).indexOf(documentID) |     return this.documents.map((d) => d.id).indexOf(documentID) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   constructor( |  | ||||||
|     private documentService: DocumentService, |  | ||||||
|     private settings: SettingsService |  | ||||||
|   ) { |  | ||||||
|     let documentListViewConfigJson = localStorage.getItem( |  | ||||||
|       DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG |  | ||||||
|     ) |  | ||||||
|     if (documentListViewConfigJson) { |  | ||||||
|       try { |  | ||||||
|         let savedState: ListViewState = JSON.parse(documentListViewConfigJson) |  | ||||||
|         // Remove null elements from the restored state |  | ||||||
|         Object.keys(savedState).forEach((k) => { |  | ||||||
|           if (savedState[k] == null) { |  | ||||||
|             delete savedState[k] |  | ||||||
|           } |  | ||||||
|         }) |  | ||||||
|         //only use restored state attributes instead of defaults if they are not null |  | ||||||
|         let newState = Object.assign(this.defaultListViewState(), savedState) |  | ||||||
|         this.listViewStates.set(null, newState) |  | ||||||
|       } catch (e) { |  | ||||||
|         localStorage.removeItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG) |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,163 +0,0 @@ | |||||||
| import { Injectable } from '@angular/core' |  | ||||||
| import { ParamMap, Params, Router } from '@angular/router' |  | ||||||
| import { FilterRule } from '../data/filter-rule' |  | ||||||
| import { FilterRuleType, FILTER_RULE_TYPES } from '../data/filter-rule-type' |  | ||||||
| import { PaperlessSavedView } from '../data/paperless-saved-view' |  | ||||||
| import { DocumentListViewService } from './document-list-view.service' |  | ||||||
|  |  | ||||||
| const SORT_FIELD_PARAMETER = 'sort' |  | ||||||
| const SORT_REVERSE_PARAMETER = 'reverse' |  | ||||||
|  |  | ||||||
| @Injectable({ |  | ||||||
|   providedIn: 'root', |  | ||||||
| }) |  | ||||||
| export class QueryParamsService { |  | ||||||
|   constructor(private router: Router, private list: DocumentListViewService) {} |  | ||||||
|  |  | ||||||
|   private filterParams: Params = {} |  | ||||||
|   private sortParams: Params = {} |  | ||||||
|  |  | ||||||
|   updateFilterRules( |  | ||||||
|     filterRules: FilterRule[], |  | ||||||
|     updateQueryParams: boolean = true |  | ||||||
|   ) { |  | ||||||
|     this.filterParams = filterRulesToQueryParams(filterRules) |  | ||||||
|     if (updateQueryParams) this.updateQueryParams() |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   set sortField(field: string) { |  | ||||||
|     this.sortParams[SORT_FIELD_PARAMETER] = field |  | ||||||
|     this.updateQueryParams() |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   set sortReverse(reverse: boolean) { |  | ||||||
|     if (!reverse) this.sortParams[SORT_REVERSE_PARAMETER] = undefined |  | ||||||
|     else this.sortParams[SORT_REVERSE_PARAMETER] = reverse |  | ||||||
|     this.updateQueryParams() |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   get params(): Params { |  | ||||||
|     return { |  | ||||||
|       ...this.sortParams, |  | ||||||
|       ...this.filterParams, |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private updateQueryParams() { |  | ||||||
|     // if we were on a saved view we navigate 'away' to /documents |  | ||||||
|     let base = [] |  | ||||||
|     if (this.router.routerState.snapshot.url.includes('/view/')) |  | ||||||
|       base = ['/documents'] |  | ||||||
|  |  | ||||||
|     this.router.navigate(base, { |  | ||||||
|       queryParams: this.params, |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public parseQueryParams(queryParams: ParamMap) { |  | ||||||
|     let filterRules = filterRulesFromQueryParams(queryParams) |  | ||||||
|     if ( |  | ||||||
|       filterRules.length || |  | ||||||
|       queryParams.has(SORT_FIELD_PARAMETER) || |  | ||||||
|       queryParams.has(SORT_REVERSE_PARAMETER) |  | ||||||
|     ) { |  | ||||||
|       this.list.filterRules = filterRules |  | ||||||
|       this.list.sortField = queryParams.get(SORT_FIELD_PARAMETER) |  | ||||||
|       this.list.sortReverse = |  | ||||||
|         queryParams.has(SORT_REVERSE_PARAMETER) || |  | ||||||
|         (!queryParams.has(SORT_FIELD_PARAMETER) && |  | ||||||
|           !queryParams.has(SORT_REVERSE_PARAMETER)) |  | ||||||
|       this.list.reload() |  | ||||||
|     } else if ( |  | ||||||
|       filterRules.length == 0 && |  | ||||||
|       !queryParams.has(SORT_FIELD_PARAMETER) |  | ||||||
|     ) { |  | ||||||
|       // this is navigating to /documents so we need to update the params from the list |  | ||||||
|       this.updateFilterRules(this.list.filterRules, false) |  | ||||||
|       this.sortParams[SORT_FIELD_PARAMETER] = this.list.sortField |  | ||||||
|       this.sortParams[SORT_REVERSE_PARAMETER] = this.list.sortReverse |  | ||||||
|       this.router.navigate([], { |  | ||||||
|         queryParams: this.params, |  | ||||||
|         replaceUrl: true, |  | ||||||
|       }) |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   updateFromView(view: PaperlessSavedView) { |  | ||||||
|     if (!this.router.routerState.snapshot.url.includes('/view/')) { |  | ||||||
|       // navigation for /documents?view= |  | ||||||
|       this.router.navigate([], { |  | ||||||
|         queryParams: { view: view.id }, |  | ||||||
|       }) |  | ||||||
|     } |  | ||||||
|     // make sure params are up-to-date |  | ||||||
|     this.updateFilterRules(view.filter_rules, false) |  | ||||||
|     this.sortParams[SORT_FIELD_PARAMETER] = this.list.sortField |  | ||||||
|     this.sortParams[SORT_REVERSE_PARAMETER] = this.list.sortReverse |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   navigateWithFilterRules(filterRules: FilterRule[]) { |  | ||||||
|     this.updateFilterRules(filterRules) |  | ||||||
|     this.router.navigate(['/documents'], { |  | ||||||
|       queryParams: this.params, |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export function filterRulesToQueryParams(filterRules: FilterRule[]): Object { |  | ||||||
|   if (filterRules) { |  | ||||||
|     let params = {} |  | ||||||
|     for (let rule of filterRules) { |  | ||||||
|       let ruleType = FILTER_RULE_TYPES.find((t) => t.id == rule.rule_type) |  | ||||||
|       if (ruleType.multi) { |  | ||||||
|         params[ruleType.filtervar] = params[ruleType.filtervar] |  | ||||||
|           ? params[ruleType.filtervar] + ',' + rule.value |  | ||||||
|           : rule.value |  | ||||||
|       } else if (ruleType.isnull_filtervar && rule.value == null) { |  | ||||||
|         params[ruleType.isnull_filtervar] = true |  | ||||||
|       } else { |  | ||||||
|         params[ruleType.filtervar] = rule.value |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     return params |  | ||||||
|   } else { |  | ||||||
|     return null |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export function filterRulesFromQueryParams(queryParams: ParamMap) { |  | ||||||
|   const allFilterRuleQueryParams: string[] = FILTER_RULE_TYPES.map( |  | ||||||
|     (rt) => rt.filtervar |  | ||||||
|   ) |  | ||||||
|     .concat(FILTER_RULE_TYPES.map((rt) => rt.isnull_filtervar)) |  | ||||||
|     .filter((rt) => rt !== undefined) |  | ||||||
|  |  | ||||||
|   // transform query params to filter rules |  | ||||||
|   let filterRulesFromQueryParams: FilterRule[] = [] |  | ||||||
|   allFilterRuleQueryParams |  | ||||||
|     .filter((frqp) => queryParams.has(frqp)) |  | ||||||
|     .forEach((filterQueryParamName) => { |  | ||||||
|       const rule_type: FilterRuleType = FILTER_RULE_TYPES.find( |  | ||||||
|         (rt) => |  | ||||||
|           rt.filtervar == filterQueryParamName || |  | ||||||
|           rt.isnull_filtervar == filterQueryParamName |  | ||||||
|       ) |  | ||||||
|       const isNullRuleType = rule_type.isnull_filtervar == filterQueryParamName |  | ||||||
|       const valueURIComponent: string = queryParams.get(filterQueryParamName) |  | ||||||
|       const filterQueryParamValues: string[] = rule_type.multi |  | ||||||
|         ? valueURIComponent.split(',') |  | ||||||
|         : [valueURIComponent] |  | ||||||
|  |  | ||||||
|       filterRulesFromQueryParams = filterRulesFromQueryParams.concat( |  | ||||||
|         // map all values to filter rules |  | ||||||
|         filterQueryParamValues.map((val) => { |  | ||||||
|           return { |  | ||||||
|             rule_type: rule_type.id, |  | ||||||
|             value: isNullRuleType ? null : val, |  | ||||||
|           } |  | ||||||
|         }) |  | ||||||
|       ) |  | ||||||
|     }) |  | ||||||
|  |  | ||||||
|   return filterRulesFromQueryParams |  | ||||||
| } |  | ||||||
| @@ -6,12 +6,12 @@ import { HttpClient, HttpParams } from '@angular/common/http' | |||||||
| import { Observable } from 'rxjs' | import { Observable } from 'rxjs' | ||||||
| import { Results } from 'src/app/data/results' | import { Results } from 'src/app/data/results' | ||||||
| import { FilterRule } from 'src/app/data/filter-rule' | import { FilterRule } from 'src/app/data/filter-rule' | ||||||
| import { map } from 'rxjs/operators' | import { map, tap } from 'rxjs/operators' | ||||||
| import { CorrespondentService } from './correspondent.service' | import { CorrespondentService } from './correspondent.service' | ||||||
| import { DocumentTypeService } from './document-type.service' | import { DocumentTypeService } from './document-type.service' | ||||||
| import { TagService } from './tag.service' | import { TagService } from './tag.service' | ||||||
| import { PaperlessDocumentSuggestions } from 'src/app/data/paperless-document-suggestions' | import { PaperlessDocumentSuggestions } from 'src/app/data/paperless-document-suggestions' | ||||||
| import { filterRulesToQueryParams } from '../query-params.service' | import { queryParamsFromFilterRules } from '../../utils/query-params' | ||||||
| import { StoragePathService } from './storage-path.service' | import { StoragePathService } from './storage-path.service' | ||||||
|  |  | ||||||
| export const DOCUMENT_SORT_FIELDS = [ | export const DOCUMENT_SORT_FIELDS = [ | ||||||
| @@ -70,7 +70,13 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument> | |||||||
|       doc.document_type$ = this.documentTypeService.getCached(doc.document_type) |       doc.document_type$ = this.documentTypeService.getCached(doc.document_type) | ||||||
|     } |     } | ||||||
|     if (doc.tags) { |     if (doc.tags) { | ||||||
|       doc.tags$ = this.tagService.getCachedMany(doc.tags) |       doc.tags$ = this.tagService | ||||||
|  |         .getCachedMany(doc.tags) | ||||||
|  |         .pipe( | ||||||
|  |           tap((tags) => | ||||||
|  |             tags.sort((tagA, tagB) => tagA.name.localeCompare(tagB.name)) | ||||||
|  |           ) | ||||||
|  |         ) | ||||||
|     } |     } | ||||||
|     if (doc.storage_path) { |     if (doc.storage_path) { | ||||||
|       doc.storage_path$ = this.storagePathService.getCached(doc.storage_path) |       doc.storage_path$ = this.storagePathService.getCached(doc.storage_path) | ||||||
| @@ -91,7 +97,7 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument> | |||||||
|       pageSize, |       pageSize, | ||||||
|       sortField, |       sortField, | ||||||
|       sortReverse, |       sortReverse, | ||||||
|       Object.assign(extraParams, filterRulesToQueryParams(filterRules)) |       Object.assign(extraParams, queryParamsFromFilterRules(filterRules)) | ||||||
|     ).pipe( |     ).pipe( | ||||||
|       map((results) => { |       map((results) => { | ||||||
|         results.results.forEach((doc) => this.addObservablesToDocument(doc)) |         results.results.forEach((doc) => this.addObservablesToDocument(doc)) | ||||||
| @@ -127,6 +133,12 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument> | |||||||
|     return url |     return url | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   update(o: PaperlessDocument): Observable<PaperlessDocument> { | ||||||
|  |     // we want to only set created_date | ||||||
|  |     o.created = undefined | ||||||
|  |     return super.update(o) | ||||||
|  |   } | ||||||
|  |  | ||||||
|   uploadDocument(formData) { |   uploadDocument(formData) { | ||||||
|     return this.http.post( |     return this.http.post( | ||||||
|       this.getResourceUrl(null, 'post_document'), |       this.getResourceUrl(null, 'post_document'), | ||||||
|   | |||||||
							
								
								
									
										71
									
								
								src-ui/src/app/services/tasks.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,71 @@ | |||||||
|  | import { HttpClient } from '@angular/common/http' | ||||||
|  | import { Injectable } from '@angular/core' | ||||||
|  | import { first, map } from 'rxjs/operators' | ||||||
|  | import { | ||||||
|  |   PaperlessTask, | ||||||
|  |   PaperlessTaskStatus, | ||||||
|  |   PaperlessTaskType, | ||||||
|  | } from 'src/app/data/paperless-task' | ||||||
|  | import { environment } from 'src/environments/environment' | ||||||
|  |  | ||||||
|  | @Injectable({ | ||||||
|  |   providedIn: 'root', | ||||||
|  | }) | ||||||
|  | export class TasksService { | ||||||
|  |   private baseUrl: string = environment.apiBaseUrl | ||||||
|  |  | ||||||
|  |   loading: boolean | ||||||
|  |  | ||||||
|  |   private fileTasks: PaperlessTask[] = [] | ||||||
|  |  | ||||||
|  |   public get total(): number { | ||||||
|  |     return this.fileTasks?.length | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public get allFileTasks(): PaperlessTask[] { | ||||||
|  |     return this.fileTasks.slice(0) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public get queuedFileTasks(): PaperlessTask[] { | ||||||
|  |     return this.fileTasks.filter((t) => t.status == PaperlessTaskStatus.Queued) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public get startedFileTasks(): PaperlessTask[] { | ||||||
|  |     return this.fileTasks.filter((t) => t.status == PaperlessTaskStatus.Started) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public get completedFileTasks(): PaperlessTask[] { | ||||||
|  |     return this.fileTasks.filter( | ||||||
|  |       (t) => t.status == PaperlessTaskStatus.Complete | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public get failedFileTasks(): PaperlessTask[] { | ||||||
|  |     return this.fileTasks.filter((t) => t.status == PaperlessTaskStatus.Failed) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   constructor(private http: HttpClient) {} | ||||||
|  |  | ||||||
|  |   public reload() { | ||||||
|  |     this.loading = true | ||||||
|  |  | ||||||
|  |     this.http | ||||||
|  |       .get<PaperlessTask[]>(`${this.baseUrl}tasks/`) | ||||||
|  |       .pipe(first()) | ||||||
|  |       .subscribe((r) => { | ||||||
|  |         this.fileTasks = r.filter((t) => t.type == PaperlessTaskType.File) // they're all File tasks, for now | ||||||
|  |         this.loading = false | ||||||
|  |       }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public dismissTasks(task_ids: Set<number>) { | ||||||
|  |     this.http | ||||||
|  |       .post(`${this.baseUrl}acknowledge_tasks/`, { | ||||||
|  |         tasks: [...task_ids], | ||||||
|  |       }) | ||||||
|  |       .pipe(first()) | ||||||
|  |       .subscribe((r) => { | ||||||
|  |         this.reload() | ||||||
|  |       }) | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,5 +0,0 @@ | |||||||
| // see https://github.com/dateutil/dateutil/issues/878 , JS Date does not |  | ||||||
| // seem to accept these strings as valid dates so we must normalize offset |  | ||||||
| export function normalizeDateStr(dateStr: string): string { |  | ||||||
|   return dateStr.replace(/[\+-](\d\d):\d\d:\d\d/gm, `-$1:00`) |  | ||||||
| } |  | ||||||
| @@ -5,11 +5,20 @@ import { NgbDateAdapter, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap' | |||||||
| export class ISODateAdapter extends NgbDateAdapter<string> { | export class ISODateAdapter extends NgbDateAdapter<string> { | ||||||
|   fromModel(value: string | null): NgbDateStruct | null { |   fromModel(value: string | null): NgbDateStruct | null { | ||||||
|     if (value) { |     if (value) { | ||||||
|       let date = new Date(value) |       if (value.match(/\d\d\d\d\-\d\d\-\d\d/g)) { | ||||||
|       return { |         const segs = value.split('-') | ||||||
|         day: date.getDate(), |         return { | ||||||
|         month: date.getMonth() + 1, |           year: parseInt(segs[0]), | ||||||
|         year: date.getFullYear(), |           month: parseInt(segs[1]), | ||||||
|  |           day: parseInt(segs[2]), | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         let date = new Date(value) | ||||||
|  |         return { | ||||||
|  |           day: date.getDate(), | ||||||
|  |           month: date.getMonth() + 1, | ||||||
|  |           year: date.getFullYear(), | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|     } else { |     } else { | ||||||
|       return null |       return null | ||||||
|   | |||||||
| @@ -1,24 +0,0 @@ | |||||||
| import { Injectable } from '@angular/core' |  | ||||||
| import { NgbDateAdapter, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap' |  | ||||||
|  |  | ||||||
| @Injectable() |  | ||||||
| export class ISODateTimeAdapter extends NgbDateAdapter<string> { |  | ||||||
|   fromModel(value: string | null): NgbDateStruct | null { |  | ||||||
|     if (value) { |  | ||||||
|       let date = new Date(value) |  | ||||||
|       return { |  | ||||||
|         day: date.getDate(), |  | ||||||
|         month: date.getMonth() + 1, |  | ||||||
|         year: date.getFullYear(), |  | ||||||
|       } |  | ||||||
|     } else { |  | ||||||
|       return null |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   toModel(date: NgbDateStruct | null): string | null { |  | ||||||
|     return date |  | ||||||
|       ? new Date(date.year, date.month - 1, date.day).toISOString() |  | ||||||
|       : null |  | ||||||
|   } |  | ||||||
| } |  | ||||||
							
								
								
									
										101
									
								
								src-ui/src/app/utils/query-params.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,101 @@ | |||||||
|  | import { ParamMap, Params } from '@angular/router' | ||||||
|  | import { FilterRule } from '../data/filter-rule' | ||||||
|  | import { FilterRuleType, FILTER_RULE_TYPES } from '../data/filter-rule-type' | ||||||
|  | import { ListViewState } from '../services/document-list-view.service' | ||||||
|  |  | ||||||
|  | const SORT_FIELD_PARAMETER = 'sort' | ||||||
|  | const SORT_REVERSE_PARAMETER = 'reverse' | ||||||
|  | const PAGE_PARAMETER = 'page' | ||||||
|  |  | ||||||
|  | export function generateParams(viewState: ListViewState): Params { | ||||||
|  |   let params = queryParamsFromFilterRules(viewState.filterRules) | ||||||
|  |   params[SORT_FIELD_PARAMETER] = viewState.sortField | ||||||
|  |   params[SORT_REVERSE_PARAMETER] = viewState.sortReverse ? 1 : undefined | ||||||
|  |   params[PAGE_PARAMETER] = isNaN(viewState.currentPage) | ||||||
|  |     ? 1 | ||||||
|  |     : viewState.currentPage | ||||||
|  |   return params | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function parseParams(queryParams: ParamMap): ListViewState { | ||||||
|  |   let filterRules = filterRulesFromQueryParams(queryParams) | ||||||
|  |   let sortField = queryParams.get(SORT_FIELD_PARAMETER) | ||||||
|  |   let sortReverse = | ||||||
|  |     queryParams.has(SORT_REVERSE_PARAMETER) || | ||||||
|  |     (!queryParams.has(SORT_FIELD_PARAMETER) && | ||||||
|  |       !queryParams.has(SORT_REVERSE_PARAMETER)) | ||||||
|  |   let currentPage = queryParams.has(PAGE_PARAMETER) | ||||||
|  |     ? parseInt(queryParams.get(PAGE_PARAMETER)) | ||||||
|  |     : 1 | ||||||
|  |   return { | ||||||
|  |     currentPage: currentPage, | ||||||
|  |     filterRules: filterRules, | ||||||
|  |     sortField: sortField, | ||||||
|  |     sortReverse: sortReverse, | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function filterRulesFromQueryParams( | ||||||
|  |   queryParams: ParamMap | ||||||
|  | ): FilterRule[] { | ||||||
|  |   const allFilterRuleQueryParams: string[] = FILTER_RULE_TYPES.map( | ||||||
|  |     (rt) => rt.filtervar | ||||||
|  |   ) | ||||||
|  |     .concat(FILTER_RULE_TYPES.map((rt) => rt.isnull_filtervar)) | ||||||
|  |     .filter((rt) => rt !== undefined) | ||||||
|  |  | ||||||
|  |   // transform query params to filter rules | ||||||
|  |   let filterRulesFromQueryParams: FilterRule[] = [] | ||||||
|  |   allFilterRuleQueryParams | ||||||
|  |     .filter((frqp) => queryParams.has(frqp)) | ||||||
|  |     .forEach((filterQueryParamName) => { | ||||||
|  |       const rule_type: FilterRuleType = FILTER_RULE_TYPES.find( | ||||||
|  |         (rt) => | ||||||
|  |           rt.filtervar == filterQueryParamName || | ||||||
|  |           rt.isnull_filtervar == filterQueryParamName | ||||||
|  |       ) | ||||||
|  |       const isNullRuleType = rule_type.isnull_filtervar == filterQueryParamName | ||||||
|  |       const valueURIComponent: string = queryParams.get(filterQueryParamName) | ||||||
|  |       const filterQueryParamValues: string[] = rule_type.multi | ||||||
|  |         ? valueURIComponent.split(',') | ||||||
|  |         : [valueURIComponent] | ||||||
|  |  | ||||||
|  |       filterRulesFromQueryParams = filterRulesFromQueryParams.concat( | ||||||
|  |         // map all values to filter rules | ||||||
|  |         filterQueryParamValues.map((val) => { | ||||||
|  |           if (rule_type.datatype == 'boolean') | ||||||
|  |             val = val.replace('1', 'true').replace('0', 'false') | ||||||
|  |           return { | ||||||
|  |             rule_type: rule_type.id, | ||||||
|  |             value: isNullRuleType ? null : val, | ||||||
|  |           } | ||||||
|  |         }) | ||||||
|  |       ) | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |   return filterRulesFromQueryParams | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function queryParamsFromFilterRules(filterRules: FilterRule[]): Params { | ||||||
|  |   if (filterRules) { | ||||||
|  |     let params = {} | ||||||
|  |     for (let rule of filterRules) { | ||||||
|  |       let ruleType = FILTER_RULE_TYPES.find((t) => t.id == rule.rule_type) | ||||||
|  |       if (ruleType.multi) { | ||||||
|  |         params[ruleType.filtervar] = params[ruleType.filtervar] | ||||||
|  |           ? params[ruleType.filtervar] + ',' + rule.value | ||||||
|  |           : rule.value | ||||||
|  |       } else if (ruleType.isnull_filtervar && rule.value == null) { | ||||||
|  |         params[ruleType.isnull_filtervar] = 1 | ||||||
|  |       } else { | ||||||
|  |         params[ruleType.filtervar] = rule.value | ||||||
|  |         if (ruleType.datatype == 'boolean') | ||||||
|  |           params[ruleType.filtervar] = | ||||||
|  |             rule.value == 'true' || rule.value == '1' ? 1 : 0 | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return params | ||||||
|  |   } else { | ||||||
|  |     return null | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,19 +1,3 @@ | |||||||
| <?xml version="1.0" encoding="utf-8"?> | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.4 238.9" style="enable-background:new 0 0 198.4 238.9" xml:space="preserve"> | ||||||
| <!-- Generator: Adobe Illustrator 25.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  --> |   <path d="M194.7 0C164.211 70.943 17.64 79.733 64.55 194.06c.59 1.468-10.848 17-18.47 29.897-1.758-6.453-3.816-13.486-3.516-14.075 38.109-45.141-27.26-70.643-30.776-107.583-16.423 29.318-22.286 80.623 27.25 110.23.29 0 2.637 11.138 3.816 16.712-1.169 2.348-2.348 4.695-2.927 6.454-1.168 2.926 7.622 2.637 7.622 3.226.879-.29 21.697-36.94 22.276-37.23C187.667 174.711 208.485 68.596 194.699 0zm-60.096 74.749c-55.11 49.246-64.49 85.897-62.732 103.777-18.47-43.682 35.772-91.76 62.732-103.777zM28.2 145.102c10.548 9.67 28.14 39.278 13.196 56.58 3.506-7.912 4.684-25.793-13.196-56.58z"/> | ||||||
| <svg version="1.1" |  | ||||||
| 	 id="svg4812" inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)" sodipodi:docname="logo-dark-notext.svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" |  | ||||||
| 	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 198.4 238.9" |  | ||||||
| 	 style="enable-background:new 0 0 198.4 238.9;" xml:space="preserve"> |  | ||||||
| <sodipodi:namedview  bordercolor="#666666" borderopacity="1.0" id="base" inkscape:current-layer="SvgjsG1020" inkscape:cx="328.04904" inkscape:cy="330.33332" inkscape:document-rotation="0" inkscape:document-units="mm" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:window-height="1016" inkscape:window-maximized="1" inkscape:window-width="1920" inkscape:window-x="1280" inkscape:window-y="27" inkscape:zoom="0.98994949" pagecolor="#ffffff" showgrid="false"> |  | ||||||
| 	</sodipodi:namedview> |  | ||||||
| <g id="layer1" transform="translate(-9.9999792,-10.000082)" inkscape:groupmode="layer" inkscape:label="Layer 1"> |  | ||||||
| 	<g id="SvgjsG1020" transform="matrix(0.10341565,0,0,0.10341565,1.2287665,8.3453496)"> |  | ||||||
| 		<path id="path57" d="M1967.5,16C1672.7,702,255.4,787,709,1892.5c5.7,14.2-104.9,164.4-178.6,289.1c-17-62.4-36.9-130.4-34-136.1 |  | ||||||
| 			c368.5-436.5-263.6-683.1-297.6-1040.3C40,1288.7-16.7,1784.8,462.3,2071.1c2.8,0,25.5,107.7,36.9,161.6 |  | ||||||
| 			c-11.3,22.7-22.7,45.4-28.3,62.4c-11.3,28.3,73.7,25.5,73.7,31.2c8.5-2.8,209.8-357.2,215.4-360 |  | ||||||
| 			C1899.5,1705.4,2100.8,679.3,1967.5,16z M1386.4,738.8C853.5,1215,762.8,1569.4,779.8,1742.3 |  | ||||||
| 			C601.2,1319.9,1125.7,855,1386.4,738.8z M357.5,1419.1c102,93.5,272.1,379.8,127.6,547.1C519,1889.7,530.4,1716.8,357.5,1419.1z" |  | ||||||
| 			/> |  | ||||||
| 	</g> |  | ||||||
| </g> |  | ||||||
| </svg> | </svg> | ||||||
|   | |||||||
| Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 727 B | 
| @@ -1,71 +1,4 @@ | |||||||
| <?xml version="1.0" encoding="utf-8"?> | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2897.4 896.6" style="enable-background:new 0 0 2897.4 896.6" xml:space="preserve"> | ||||||
| <!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  --> |   <path d="M1022.3 428.7c-17.8-19.9-42.7-29.8-74.7-29.8-22.3 0-42.4 5.7-60.5 17.3-18.1 11.6-32.3 27.5-42.5 47.8s-15.3 42.9-15.3 67.8 5.1 47.5 15.3 67.8c10.3 20.3 24.4 36.2 42.5 47.8 18.1 11.5 38.3 17.3 60.5 17.3 32 0 56.9-9.9 74.7-29.8V655.5h84.5V408.3h-84.5v20.4zM1010.5 575c-10.2 11.7-23.6 17.6-40.2 17.6s-29.9-5.9-40-17.6-15.1-26.1-15.1-43.3c0-17.1 5-31.6 15.1-43.3s23.4-17.6 40-17.6 30 5.9 40.2 17.6 15.3 26.1 15.3 43.3-5.1 31.6-15.3 43.3zM1381 416.1c-18.1-11.5-38.3-17.3-60.5-17.4-32 0-56.9 9.9-74.7 29.8v-20.4h-84.5v390.7h84.5v-164c17.8 19.9 42.7 29.8 74.7 29.8 22.3 0 42.4-5.7 60.5-17.3s32.3-27.5 42.5-47.8c10.2-20.3 15.3-42.9 15.3-67.8s-5.1-47.5-15.3-67.8c-10.3-20.3-24.4-36.2-42.5-47.8zM1337.9 575c-10.1 11.7-23.4 17.6-40 17.6s-29.9-5.9-40-17.6-15.1-26.1-15.1-43.3c0-17.1 5-31.6 15.1-43.3s23.4-17.6 40-17.6 29.9 5.9 40 17.6 15.1 26.1 15.1 43.3-5.1 31.6-15.1 43.3zM1672.2 416.8c-20.5-12-43-18-67.6-18-24.9 0-47.6 5.9-68 17.6-20.4 11.7-36.5 27.7-48.2 48s-17.6 42.7-17.6 67.3c.3 25.2 6.2 47.8 17.8 68 11.5 20.2 28 36 49.3 47.6 21.3 11.5 45.9 17.3 73.8 17.3 48.6 0 86.8-14.7 114.7-44l-52.5-48.9c-8.6 8.3-17.6 14.6-26.7 19-9.3 4.3-21.1 6.4-35.3 6.4-11.6 0-22.5-3.6-32.7-10.9-10.3-7.3-17.1-16.5-20.7-27.8h180l.4-11.6c0-29.6-6-55.7-18-78.2s-28.3-39.8-48.7-51.8zm-113.9 86.4c2.1-12.1 7.5-21.8 16.2-29.1s18.7-10.9 30-10.9 21.2 3.6 29.8 10.9c8.6 7.2 13.9 16.9 16 29.1h-92zM1895.3 411.7c-11 5.6-20.3 13.7-28 24.4h-.1v-28h-84.5v247.3h84.5V536.3c0-22.6 4.7-38.1 14.2-46.5 9.5-8.5 22.7-12.7 39.6-12.7 6.2 0 13.5 1 21.8 3.1l10.7-72c-5.9-3.3-14.5-4.9-25.8-4.9-10.6 0-21.4 2.8-32.4 8.4zM1985 277.4h84.5v377.8H1985zM2313.2 416.8c-20.5-12-43-18-67.6-18-24.9 0-47.6 5.9-68 17.6s-36.5 27.7-48.2 48c-11.7 20.3-17.6 42.7-17.6 67.3.3 25.2 6.2 47.8 17.8 68 11.5 20.2 28 36 49.3 47.6 21.3 11.5 45.9 17.3 73.8 17.3 48.6 0 86.8-14.7 114.7-44l-52.5-48.9c-8.6 8.3-17.6 14.6-26.7 19-9.3 4.3-21.1 6.4-35.3 6.4-11.6 0-22.5-3.6-32.7-10.9-10.3-7.3-17.1-16.5-20.7-27.8h180l.4-11.6c0-29.6-6-55.7-18-78.2s-28.3-39.8-48.7-51.8zm-113.9 86.4c2.1-12.1 7.5-21.8 16.2-29.1s18.7-10.9 30-10.9 21.2 3.6 29.8 10.9c8.6 7.2 13.9 16.9 16 29.1h-92zM2583.6 507.7c-13.8-4.4-30.6-8.1-50.5-11.1-15.1-2.7-26.1-5.2-32.9-7.6-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7 6.7-10.9c4.4-2.2 11.5-3.3 21.3-3.3 11.6 0 24.3 2.4 38.1 7.2 13.9 4.8 26.2 11 36.9 18.4l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8-18.7-7.1-39.6-10.7-62.7-10.7-33.7 0-60.2 7.6-79.3 22.7-19.1 15.1-28.7 36.1-28.7 63.1 0 19 4.8 33.9 14.4 44.7 9.6 10.8 21 18.5 34 22.9 13.1 4.5 28.9 8.3 47.6 11.6 14.6 2.7 25.1 5.3 31.6 7.8s9.8 6.5 9.8 11.8c0 10.4-9.7 15.6-29.3 15.6-13.7 0-28.5-2.3-44.7-6.9-16.1-4.6-29.2-11.3-39.3-20.2l-33.3 60c9.2 7.4 24.6 14.7 46.2 22 21.7 7.3 45.2 10.9 70.7 10.9 34.7 0 62.9-7.4 84.5-22.4 21.7-15 32.5-37.3 32.5-66.9 0-19.3-5-34.2-15.1-44.9s-22-18.3-35.8-22.7zM2883.4 575.3c0-19.3-5-34.2-15.1-44.9s-22-18.3-35.8-22.7c-13.8-4.4-30.6-8.1-50.5-11.1-15.1-2.7-26.1-5.2-32.9-7.6-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7 6.7-10.9c4.4-2.2 11.5-3.3 21.3-3.3 11.6 0 24.3 2.4 38.1 7.2 13.9 4.8 26.2 11 36.9 18.4l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8-18.7-7.1-39.6-10.7-62.7-10.7-33.7 0-60.2 7.6-79.3 22.7-19.1 15.1-28.7 36.1-28.7 63.1 0 19 4.8 33.9 14.4 44.7 9.6 10.8 21 18.5 34 22.9 13.1 4.5 28.9 8.3 47.6 11.6 14.6 2.7 25.1 5.3 31.6 7.8s9.8 6.5 9.8 11.8c0 10.4-9.7 15.6-29.3 15.6-13.7 0-28.5-2.3-44.7-6.9-16.1-4.6-29.2-11.3-39.3-20.2l-33.3 60c9.2 7.4 24.6 14.7 46.2 22 21.7 7.3 45.2 10.9 70.7 10.9 34.7 0 62.9-7.4 84.5-22.4 21.7-15 32.5-37.3 32.5-66.9zM2460.7 738.7h59.6v17.2h-59.6zM2596.5 706.4c-5.7 0-11 1-15.8 3s-9 5-12.5 8.9v-9.4h-19.4v93.6h19.4v-52c0-8.6 2.1-15.3 6.3-20 4.2-4.7 9.5-7.1 15.9-7.1 7.8 0 13.4 2.3 16.8 6.7 3.4 4.5 5.1 11.3 5.1 20.5v52h19.4v-56.8c0-12.8-3.2-22.6-9.5-29.3-6.4-6.7-14.9-10.1-25.7-10.1zM2733.8 717.7c-3.6-3.4-7.9-6.1-13.1-8.2s-10.6-3.1-16.2-3.1c-8.7 0-16.5 2.1-23.5 6.3s-12.5 10-16.5 17.3c-4 7.3-6 15.4-6 24.4 0 8.9 2 17.1 6 24.3 4 7.3 9.5 13 16.5 17.2s14.9 6.3 23.5 6.3c5.6 0 11-1 16.2-3.1 5.1-2.1 9.5-4.8 13.1-8.2v24.4c0 8.5-2.5 14.8-7.6 18.7-5 3.9-11 5.9-18 5.9-6.7 0-12.4-1.6-17.3-4.7-4.8-3.1-7.6-7.7-8.3-13.8h-19.4c.6 7.7 2.9 14.2 7.1 19.5s9.6 9.3 16.2 12c6.6 2.7 13.8 4 21.7 4 12.8 0 23.5-3.4 32-10.1 8.6-6.7 12.8-17.1 12.8-31.1V708.9h-19.2v8.8zm-1.6 52.4c-2.5 4.7-6 8.3-10.4 11.2-4.4 2.7-9.4 4-14.9 4-5.7 0-10.8-1.4-15.2-4.3s-7.8-6.7-10.2-11.4c-2.3-4.8-3.5-9.8-3.5-15.2 0-5.5 1.1-10.6 3.5-15.3s5.8-8.5 10.2-11.3 9.5-4.2 15.2-4.2c5.5 0 10.5 1.4 14.9 4s7.9 6.3 10.4 11 3.8 10 3.8 15.8-1.3 11-3.8 15.7zM2867.9 708.9h-21.4l-25.6 33-25.4-33h-22.4l36 46.1-37.6 47.5h21.4l27.2-34.6 27.1 34.7h22.4l-37.6-48.2zM757.6 293.7c-20-10.8-42.6-16.2-67.8-16.2H600c-8.5 39.2-21.1 76.4-37.6 111.3-9.9 20.8-21.1 40.6-33.6 59.4v207.2h88.9V521.5h72c25.2 0 47.8-5.4 67.8-16.2s35.7-25.6 47.1-44.2c11.4-18.7 17.1-39.1 17.1-61.3.1-22.7-5.6-43.3-17-61.9-11.4-18.7-27.1-33.4-47.1-44.2zm-41 140.6c-9.3 8.9-21.6 13.3-36.7 13.3l-62.2.4v-92.5l62.2-.4c15.1 0 27.3 4.4 36.7 13.3 9.4 8.9 14 19.9 14 32.9 0 13.2-4.6 24.1-14 33z"/> | ||||||
| <svg version="1.1" |   <path d="M140 713.7c-3.4-16.4-10.3-49.1-11.2-49.1C-16.9 577.5.4 426.6 48.6 340.4 59 449 251.2 524 139.1 656.8c-.9 1.7 5.2 22.4 10.3 41.4 22.4-37.9 56-83.6 54.3-87.9C65.9 273.9 496.9 248.1 586.6 39.4c40.5 201.8-20.7 513.9-367.2 593.2-1.7.9-62.9 108.6-65.5 109.5 0-1.7-25.9-.9-22.4-9.5 1.6-5.2 5.1-12 8.5-18.9zm-4.3-81.1c44-50.9-7.8-137.9-38.8-166.4 52.6 90.5 49.1 143.1 38.8 166.4z" style="fill:#17541f"/> | ||||||
| 	 id="svg9580" inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)" sodipodi:docname="logo.svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" |  | ||||||
| 	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 2897.4 896.6" |  | ||||||
| 	 style="enable-background:new 0 0 2897.4 896.6;" xml:space="preserve"> |  | ||||||
| <style type="text/css"> |  | ||||||
| 	.st0{fill:#17541F;} |  | ||||||
| </style> |  | ||||||
| <sodipodi:namedview  bordercolor="#666666" borderopacity="1" gridtolerance="10" guidetolerance="10" id="namedview9582" inkscape:current-layer="g9578" inkscape:cx="1393.617" inkscape:cy="393.61704" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-height="1016" inkscape:window-maximized="1" inkscape:window-width="1920" inkscape:window-x="1280" inkscape:window-y="27" inkscape:zoom="0.46439736" objecttolerance="10" pagecolor="#ffffff" showgrid="false"> |  | ||||||
| 	</sodipodi:namedview> |  | ||||||
| <g> |  | ||||||
| 	<path d="M1022.3,428.7c-17.8-19.9-42.7-29.8-74.7-29.8c-22.3,0-42.4,5.7-60.5,17.3c-18.1,11.6-32.3,27.5-42.5,47.8 |  | ||||||
| 		s-15.3,42.9-15.3,67.8c0,24.9,5.1,47.5,15.3,67.8c10.3,20.3,24.4,36.2,42.5,47.8c18.1,11.5,38.3,17.3,60.5,17.3 |  | ||||||
| 		c32,0,56.9-9.9,74.7-29.8v20.4v0.2h84.5V408.3h-84.5V428.7z M1010.5,575c-10.2,11.7-23.6,17.6-40.2,17.6s-29.9-5.9-40-17.6 |  | ||||||
| 		s-15.1-26.1-15.1-43.3c0-17.1,5-31.6,15.1-43.3s23.4-17.6,40-17.6c16.6,0,30,5.9,40.2,17.6s15.3,26.1,15.3,43.3 |  | ||||||
| 		S1020.7,563.3,1010.5,575z"/> |  | ||||||
| 	<path d="M1381,416.1c-18.1-11.5-38.3-17.3-60.5-17.4c-32,0-56.9,9.9-74.7,29.8v-20.4h-84.5v390.7h84.5v-164 |  | ||||||
| 		c17.8,19.9,42.7,29.8,74.7,29.8c22.3,0,42.4-5.7,60.5-17.3s32.3-27.5,42.5-47.8c10.2-20.3,15.3-42.9,15.3-67.8s-5.1-47.5-15.3-67.8 |  | ||||||
| 		C1413.2,443.6,1399.1,427.7,1381,416.1z M1337.9,575c-10.1,11.7-23.4,17.6-40,17.6s-29.9-5.9-40-17.6s-15.1-26.1-15.1-43.3 |  | ||||||
| 		c0-17.1,5-31.6,15.1-43.3s23.4-17.6,40-17.6s29.9,5.9,40,17.6s15.1,26.1,15.1,43.3S1347.9,563.3,1337.9,575z"/> |  | ||||||
| 	<path d="M1672.2,416.8c-20.5-12-43-18-67.6-18c-24.9,0-47.6,5.9-68,17.6c-20.4,11.7-36.5,27.7-48.2,48s-17.6,42.7-17.6,67.3 |  | ||||||
| 		c0.3,25.2,6.2,47.8,17.8,68c11.5,20.2,28,36,49.3,47.6c21.3,11.5,45.9,17.3,73.8,17.3c48.6,0,86.8-14.7,114.7-44l-52.5-48.9 |  | ||||||
| 		c-8.6,8.3-17.6,14.6-26.7,19c-9.3,4.3-21.1,6.4-35.3,6.4c-11.6,0-22.5-3.6-32.7-10.9c-10.3-7.3-17.1-16.5-20.7-27.8h180l0.4-11.6 |  | ||||||
| 		c0-29.6-6-55.7-18-78.2S1692.6,428.8,1672.2,416.8z M1558.3,503.2c2.1-12.1,7.5-21.8,16.2-29.1s18.7-10.9,30-10.9 |  | ||||||
| 		s21.2,3.6,29.8,10.9c8.6,7.2,13.9,16.9,16,29.1H1558.3z"/> |  | ||||||
| 	<path d="M1895.3,411.7c-11,5.6-20.3,13.7-28,24.4h-0.1v-28h-84.5v247.3h84.5V536.3c0-22.6,4.7-38.1,14.2-46.5 |  | ||||||
| 		c9.5-8.5,22.7-12.7,39.6-12.7c6.2,0,13.5,1,21.8,3.1l10.7-72c-5.9-3.3-14.5-4.9-25.8-4.9C1917.1,403.3,1906.3,406.1,1895.3,411.7z" |  | ||||||
| 		/> |  | ||||||
| 	<rect x="1985" y="277.4" width="84.5" height="377.8"/> |  | ||||||
| 	<path d="M2313.2,416.8c-20.5-12-43-18-67.6-18c-24.9,0-47.6,5.9-68,17.6s-36.5,27.7-48.2,48c-11.7,20.3-17.6,42.7-17.6,67.3 |  | ||||||
| 		c0.3,25.2,6.2,47.8,17.8,68c11.5,20.2,28,36,49.3,47.6c21.3,11.5,45.9,17.3,73.8,17.3c48.6,0,86.8-14.7,114.7-44l-52.5-48.9 |  | ||||||
| 		c-8.6,8.3-17.6,14.6-26.7,19c-9.3,4.3-21.1,6.4-35.3,6.4c-11.6,0-22.5-3.6-32.7-10.9c-10.3-7.3-17.1-16.5-20.7-27.8h180l0.4-11.6 |  | ||||||
| 		c0-29.6-6-55.7-18-78.2S2333.6,428.8,2313.2,416.8z M2199.3,503.2c2.1-12.1,7.5-21.8,16.2-29.1s18.7-10.9,30-10.9 |  | ||||||
| 		s21.2,3.6,29.8,10.9c8.6,7.2,13.9,16.9,16,29.1H2199.3z"/> |  | ||||||
| 	<path d="M2583.6,507.7c-13.8-4.4-30.6-8.1-50.5-11.1c-15.1-2.7-26.1-5.2-32.9-7.6c-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7,6.7-10.9 |  | ||||||
| 		c4.4-2.2,11.5-3.3,21.3-3.3c11.6,0,24.3,2.4,38.1,7.2c13.9,4.8,26.2,11,36.9,18.4l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8 |  | ||||||
| 		c-18.7-7.1-39.6-10.7-62.7-10.7c-33.7,0-60.2,7.6-79.3,22.7c-19.1,15.1-28.7,36.1-28.7,63.1c0,19,4.8,33.9,14.4,44.7 |  | ||||||
| 		c9.6,10.8,21,18.5,34,22.9c13.1,4.5,28.9,8.3,47.6,11.6c14.6,2.7,25.1,5.3,31.6,7.8s9.8,6.5,9.8,11.8c0,10.4-9.7,15.6-29.3,15.6 |  | ||||||
| 		c-13.7,0-28.5-2.3-44.7-6.9c-16.1-4.6-29.2-11.3-39.3-20.2l-33.3,60c9.2,7.4,24.6,14.7,46.2,22c21.7,7.3,45.2,10.9,70.7,10.9 |  | ||||||
| 		c34.7,0,62.9-7.4,84.5-22.4c21.7-15,32.5-37.3,32.5-66.9c0-19.3-5-34.2-15.1-44.9S2597.4,512.1,2583.6,507.7z"/> |  | ||||||
| 	<path d="M2883.4,575.3c0-19.3-5-34.2-15.1-44.9s-22-18.3-35.8-22.7c-13.8-4.4-30.6-8.1-50.5-11.1c-15.1-2.7-26.1-5.2-32.9-7.6 |  | ||||||
| 		c-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7,6.7-10.9c4.4-2.2,11.5-3.3,21.3-3.3c11.6,0,24.3,2.4,38.1,7.2c13.9,4.8,26.2,11,36.9,18.4 |  | ||||||
| 		l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8c-18.7-7.1-39.6-10.7-62.7-10.7c-33.7,0-60.2,7.6-79.3,22.7 |  | ||||||
| 		c-19.1,15.1-28.7,36.1-28.7,63.1c0,19,4.8,33.9,14.4,44.7c9.6,10.8,21,18.5,34,22.9c13.1,4.5,28.9,8.3,47.6,11.6 |  | ||||||
| 		c14.6,2.7,25.1,5.3,31.6,7.8s9.8,6.5,9.8,11.8c0,10.4-9.7,15.6-29.3,15.6c-13.7,0-28.5-2.3-44.7-6.9c-16.1-4.6-29.2-11.3-39.3-20.2 |  | ||||||
| 		l-33.3,60c9.2,7.4,24.6,14.7,46.2,22c21.7,7.3,45.2,10.9,70.7,10.9c34.7,0,62.9-7.4,84.5-22.4 |  | ||||||
| 		C2872.6,627.2,2883.4,604.9,2883.4,575.3z"/> |  | ||||||
| 	<rect x="2460.7" y="738.7" width="59.6" height="17.2"/> |  | ||||||
| 	<path d="M2596.5,706.4c-5.7,0-11,1-15.8,3s-9,5-12.5,8.9v-9.4h-19.4v93.6h19.4v-52c0-8.6,2.1-15.3,6.3-20c4.2-4.7,9.5-7.1,15.9-7.1 |  | ||||||
| 		c7.8,0,13.4,2.3,16.8,6.7c3.4,4.5,5.1,11.3,5.1,20.5v52h19.4v-56.8c0-12.8-3.2-22.6-9.5-29.3 |  | ||||||
| 		C2615.8,709.8,2607.3,706.4,2596.5,706.4z"/> |  | ||||||
| 	<path d="M2733.8,717.7c-3.6-3.4-7.9-6.1-13.1-8.2s-10.6-3.1-16.2-3.1c-8.7,0-16.5,2.1-23.5,6.3s-12.5,10-16.5,17.3 |  | ||||||
| 		c-4,7.3-6,15.4-6,24.4c0,8.9,2,17.1,6,24.3c4,7.3,9.5,13,16.5,17.2s14.9,6.3,23.5,6.3c5.6,0,11-1,16.2-3.1 |  | ||||||
| 		c5.1-2.1,9.5-4.8,13.1-8.2v24.4c0,8.5-2.5,14.8-7.6,18.7c-5,3.9-11,5.9-18,5.9c-6.7,0-12.4-1.6-17.3-4.7c-4.8-3.1-7.6-7.7-8.3-13.8 |  | ||||||
| 		h-19.4c0.6,7.7,2.9,14.2,7.1,19.5s9.6,9.3,16.2,12c6.6,2.7,13.8,4,21.7,4c12.8,0,23.5-3.4,32-10.1c8.6-6.7,12.8-17.1,12.8-31.1 |  | ||||||
| 		V708.9h-19.2V717.7z M2732.2,770.1c-2.5,4.7-6,8.3-10.4,11.2c-4.4,2.7-9.4,4-14.9,4c-5.7,0-10.8-1.4-15.2-4.3s-7.8-6.7-10.2-11.4 |  | ||||||
| 		c-2.3-4.8-3.5-9.8-3.5-15.2c0-5.5,1.1-10.6,3.5-15.3s5.8-8.5,10.2-11.3s9.5-4.2,15.2-4.2c5.5,0,10.5,1.4,14.9,4s7.9,6.3,10.4,11 |  | ||||||
| 		s3.8,10,3.8,15.8S2734.7,765.4,2732.2,770.1z"/> |  | ||||||
| 	<polygon points="2867.9,708.9 2846.5,708.9 2820.9,741.9 2795.5,708.9 2773.1,708.9 2809.1,755 2771.5,802.5 2792.9,802.5  |  | ||||||
| 		2820.1,767.9 2847.2,802.6 2869.6,802.6 2832,754.4 	"/> |  | ||||||
| 	<path d="M757.6,293.7c-20-10.8-42.6-16.2-67.8-16.2H600c-8.5,39.2-21.1,76.4-37.6,111.3c-9.9,20.8-21.1,40.6-33.6,59.4v207.2h88.9 |  | ||||||
| 		V521.5h72c25.2,0,47.8-5.4,67.8-16.2s35.7-25.6,47.1-44.2c11.4-18.7,17.1-39.1,17.1-61.3c0.1-22.7-5.6-43.3-17-61.9 |  | ||||||
| 		C793.3,319.2,777.6,304.5,757.6,293.7z M716.6,434.3c-9.3,8.9-21.6,13.3-36.7,13.3l-62.2,0.4v-92.5l62.2-0.4 |  | ||||||
| 		c15.1,0,27.3,4.4,36.7,13.3c9.4,8.9,14,19.9,14,32.9C730.6,414.5,726,425.4,716.6,434.3z"/> |  | ||||||
| </g> |  | ||||||
| <path class="st0" d="M140,713.7c-3.4-16.4-10.3-49.1-11.2-49.1c-145.7-87.1-128.4-238-80.2-324.2C59,449,251.2,524,139.1,656.8 |  | ||||||
| 	c-0.9,1.7,5.2,22.4,10.3,41.4c22.4-37.9,56-83.6,54.3-87.9C65.9,273.9,496.9,248.1,586.6,39.4c40.5,201.8-20.7,513.9-367.2,593.2 |  | ||||||
| 	c-1.7,0.9-62.9,108.6-65.5,109.5c0-1.7-25.9-0.9-22.4-9.5C133.1,727.4,136.6,720.6,140,713.7L140,713.7z M135.7,632.6 |  | ||||||
| 	c44-50.9-7.8-137.9-38.8-166.4C149.5,556.7,146,609.3,135.7,632.6L135.7,632.6z"/> |  | ||||||
| </svg> | </svg> | ||||||
|   | |||||||
| Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 5.4 KiB | 
							
								
								
									
										3
									
								
								src-ui/src/assets/logo-notext.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | |||||||
|  | <svg xmlns="http://www.w3.org/2000/svg" width="264.567" height="318.552" viewBox="0 0 70 84.284"> | ||||||
|  |   <path style="fill:#17541f;stroke-width:1.10017" d="M752.438 82.365C638.02 348.605 87.938 381.61 263.964 810.674c2.2 5.5-40.706 63.81-69.31 112.217-6.602-24.204-14.304-50.607-13.204-52.807C324.473 700.658 79.136 604.944 65.934 466.322 4.324 576.34-17.678 768.868 168.25 879.984c1.1 0 9.902 41.808 14.303 62.711-4.4 8.802-8.802 17.602-11.002 24.203-4.4 11.002 28.603 9.902 28.603 12.102 3.3-1.1 81.413-138.62 83.614-139.72 442.267-101.216 520.377-499.476 468.67-756.915ZM526.904 362.906c-206.831 184.828-242.036 322.35-235.435 389.46-69.31-163.926 134.22-344.353 235.435-389.46ZM127.543 626.947c39.606 36.306 105.616 147.422 49.508 212.332 13.202-29.704 17.602-96.814-49.508-212.332z" transform="matrix(.094 0 0 .094 -2.042 -7.742)" fill="#17541F"/> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 855 B | 
| @@ -1,69 +1,3 @@ | |||||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | <svg xmlns="http://www.w3.org/2000/svg" width="264.567" height="318.552" viewBox="0 0 70 84.284"> | ||||||
| <svg |   <path style="fill:#fff;stroke-width:1.10017" d="M752.438 82.365C638.02 348.605 87.938 381.61 263.964 810.674c2.2 5.5-40.706 63.81-69.31 112.217-6.602-24.204-14.304-50.607-13.204-52.807C324.473 700.658 79.136 604.944 65.934 466.322 4.324 576.34-17.678 768.868 168.25 879.984c1.1 0 9.902 41.808 14.303 62.711-4.4 8.802-8.802 17.602-11.002 24.203-4.4 11.002 28.603 9.902 28.603 12.102 3.3-1.1 81.413-138.62 83.614-139.72 442.267-101.216 520.377-499.476 468.67-756.915ZM526.904 362.906c-206.831 184.828-242.036 322.35-235.435 389.46-69.31-163.926 134.22-344.353 235.435-389.46ZM127.543 626.947c39.606 36.306 105.616 147.422 49.508 212.332 13.202-29.704 17.602-96.814-49.508-212.332z" transform="matrix(.094 0 0 .094 -2.042 -7.742)" fill="#fff"/> | ||||||
|    xmlns:dc="http://purl.org/dc/elements/1.1/" |  | ||||||
|    xmlns:cc="http://creativecommons.org/ns#" |  | ||||||
|    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" |  | ||||||
|    xmlns:svg="http://www.w3.org/2000/svg" |  | ||||||
|    xmlns="http://www.w3.org/2000/svg" |  | ||||||
|    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" |  | ||||||
|    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" |  | ||||||
|    width="69.999977mm" |  | ||||||
|    height="84.283669mm" |  | ||||||
|    viewBox="0 0 69.999977 84.283669" |  | ||||||
|    version="1.1" |  | ||||||
|    id="svg4812" |  | ||||||
|    inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)" |  | ||||||
|    sodipodi:docname="logo-dark-notext.svg"> |  | ||||||
|   <defs |  | ||||||
|      id="defs4806" /> |  | ||||||
|   <sodipodi:namedview |  | ||||||
|      id="base" |  | ||||||
|      pagecolor="#ffffff" |  | ||||||
|      bordercolor="#666666" |  | ||||||
|      borderopacity="1.0" |  | ||||||
|      inkscape:pageopacity="0.0" |  | ||||||
|      inkscape:pageshadow="2" |  | ||||||
|      inkscape:zoom="0.98994949" |  | ||||||
|      inkscape:cx="328.04904" |  | ||||||
|      inkscape:cy="330.33332" |  | ||||||
|      inkscape:document-units="mm" |  | ||||||
|      inkscape:current-layer="SvgjsG1020" |  | ||||||
|      inkscape:document-rotation="0" |  | ||||||
|      showgrid="false" |  | ||||||
|      inkscape:window-width="1920" |  | ||||||
|      inkscape:window-height="1016" |  | ||||||
|      inkscape:window-x="1280" |  | ||||||
|      inkscape:window-y="27" |  | ||||||
|      inkscape:window-maximized="1" /> |  | ||||||
|   <metadata |  | ||||||
|      id="metadata4809"> |  | ||||||
|     <rdf:RDF> |  | ||||||
|       <cc:Work |  | ||||||
|          rdf:about=""> |  | ||||||
|         <dc:format>image/svg+xml</dc:format> |  | ||||||
|         <dc:type |  | ||||||
|            rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> |  | ||||||
|         <dc:title></dc:title> |  | ||||||
|       </cc:Work> |  | ||||||
|     </rdf:RDF> |  | ||||||
|   </metadata> |  | ||||||
|   <g |  | ||||||
|      inkscape:label="Layer 1" |  | ||||||
|      inkscape:groupmode="layer" |  | ||||||
|      id="layer1" |  | ||||||
|      transform="translate(-9.9999792,-10.000082)"> |  | ||||||
|     <g |  | ||||||
|        id="SvgjsG1020" |  | ||||||
|        featureKey="symbol1" |  | ||||||
|        fill="#ffffff" |  | ||||||
|        transform="matrix(0.10341565,0,0,0.10341565,1.2287665,8.3453496)"> |  | ||||||
|       <path |  | ||||||
|          id="path57" |  | ||||||
|          style="fill:#ffffff;stroke-width:1.10017" |  | ||||||
|          d="M 752.4375,82.365234 C 638.02019,348.60552 87.938206,381.6089 263.96484,810.67383 c 2.20034,5.50083 -40.70621,63.80947 -69.31054,112.21679 -6.601,-24.20366 -14.30329,-50.6063 -13.20313,-52.80664 C 324.47281,700.65835 79.135592,604.94324 65.933594,466.32227 4.3242706,576.33891 -17.678136,768.86756 168.25,879.98438 c 1.10017,-10e-6 9.90207,41.80777 14.30273,62.71093 -4.40066,8.80133 -8.80162,17.60213 -11.00195,24.20313 -4.40066,11.00166 28.60352,9.90123 28.60352,12.10156 3.3005,-1.10017 81.41295,-138.62054 83.61328,-139.7207 C 726.0345,738.06398 804.14532,339.80419 752.4375,82.365234 Z M 526.9043,362.90625 C 320.073,547.73422 284.86775,685.25508 291.46875,752.36523 222.15826,588.44043 425.68898,408.01308 526.9043,362.90625 Z M 127.54297,626.94727 c 39.60599,36.30549 105.6163,147.4222 49.50781,212.33203 13.202,-29.7045 17.60234,-96.81455 -49.50781,-212.33203 z" |  | ||||||
|          transform="matrix(0.90895334,0,0,0.90895334,65.06894,-58.865357)" /> |  | ||||||
|       <defs |  | ||||||
|          id="defs14302" /> |  | ||||||
|     </g> |  | ||||||
|   </g> |  | ||||||
| </svg> | </svg> | ||||||
|   | |||||||
| Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 849 B | 
| @@ -1,71 +1,4 @@ | |||||||
| <?xml version="1.0" encoding="utf-8"?> | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2897.4 896.6" style="enable-background:new 0 0 2897.4 896.6" xml:space="preserve"> | ||||||
| <!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  --> |   <path d="M1022.3 428.7c-17.8-19.9-42.7-29.8-74.7-29.8-22.3 0-42.4 5.7-60.5 17.3-18.1 11.6-32.3 27.5-42.5 47.8s-15.3 42.9-15.3 67.8 5.1 47.5 15.3 67.8c10.3 20.3 24.4 36.2 42.5 47.8 18.1 11.5 38.3 17.3 60.5 17.3 32 0 56.9-9.9 74.7-29.8V655.5h84.5V408.3h-84.5v20.4zM1010.5 575c-10.2 11.7-23.6 17.6-40.2 17.6s-29.9-5.9-40-17.6-15.1-26.1-15.1-43.3c0-17.1 5-31.6 15.1-43.3s23.4-17.6 40-17.6 30 5.9 40.2 17.6 15.3 26.1 15.3 43.3-5.1 31.6-15.3 43.3zM1381 416.1c-18.1-11.5-38.3-17.3-60.5-17.4-32 0-56.9 9.9-74.7 29.8v-20.4h-84.5v390.7h84.5v-164c17.8 19.9 42.7 29.8 74.7 29.8 22.3 0 42.4-5.7 60.5-17.3s32.3-27.5 42.5-47.8c10.2-20.3 15.3-42.9 15.3-67.8s-5.1-47.5-15.3-67.8c-10.3-20.3-24.4-36.2-42.5-47.8zM1337.9 575c-10.1 11.7-23.4 17.6-40 17.6s-29.9-5.9-40-17.6-15.1-26.1-15.1-43.3c0-17.1 5-31.6 15.1-43.3s23.4-17.6 40-17.6 29.9 5.9 40 17.6 15.1 26.1 15.1 43.3-5.1 31.6-15.1 43.3zM1672.2 416.8c-20.5-12-43-18-67.6-18-24.9 0-47.6 5.9-68 17.6-20.4 11.7-36.5 27.7-48.2 48s-17.6 42.7-17.6 67.3c.3 25.2 6.2 47.8 17.8 68 11.5 20.2 28 36 49.3 47.6 21.3 11.5 45.9 17.3 73.8 17.3 48.6 0 86.8-14.7 114.7-44l-52.5-48.9c-8.6 8.3-17.6 14.6-26.7 19-9.3 4.3-21.1 6.4-35.3 6.4-11.6 0-22.5-3.6-32.7-10.9-10.3-7.3-17.1-16.5-20.7-27.8h180l.4-11.6c0-29.6-6-55.7-18-78.2s-28.3-39.8-48.7-51.8zm-113.9 86.4c2.1-12.1 7.5-21.8 16.2-29.1s18.7-10.9 30-10.9 21.2 3.6 29.8 10.9c8.6 7.2 13.9 16.9 16 29.1h-92zM1895.3 411.7c-11 5.6-20.3 13.7-28 24.4h-.1v-28h-84.5v247.3h84.5V536.3c0-22.6 4.7-38.1 14.2-46.5 9.5-8.5 22.7-12.7 39.6-12.7 6.2 0 13.5 1 21.8 3.1l10.7-72c-5.9-3.3-14.5-4.9-25.8-4.9-10.6 0-21.4 2.8-32.4 8.4zM1985 277.4h84.5v377.8H1985zM2313.2 416.8c-20.5-12-43-18-67.6-18-24.9 0-47.6 5.9-68 17.6s-36.5 27.7-48.2 48c-11.7 20.3-17.6 42.7-17.6 67.3.3 25.2 6.2 47.8 17.8 68 11.5 20.2 28 36 49.3 47.6 21.3 11.5 45.9 17.3 73.8 17.3 48.6 0 86.8-14.7 114.7-44l-52.5-48.9c-8.6 8.3-17.6 14.6-26.7 19-9.3 4.3-21.1 6.4-35.3 6.4-11.6 0-22.5-3.6-32.7-10.9-10.3-7.3-17.1-16.5-20.7-27.8h180l.4-11.6c0-29.6-6-55.7-18-78.2s-28.3-39.8-48.7-51.8zm-113.9 86.4c2.1-12.1 7.5-21.8 16.2-29.1s18.7-10.9 30-10.9 21.2 3.6 29.8 10.9c8.6 7.2 13.9 16.9 16 29.1h-92zM2583.6 507.7c-13.8-4.4-30.6-8.1-50.5-11.1-15.1-2.7-26.1-5.2-32.9-7.6-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7 6.7-10.9c4.4-2.2 11.5-3.3 21.3-3.3 11.6 0 24.3 2.4 38.1 7.2 13.9 4.8 26.2 11 36.9 18.4l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8-18.7-7.1-39.6-10.7-62.7-10.7-33.7 0-60.2 7.6-79.3 22.7-19.1 15.1-28.7 36.1-28.7 63.1 0 19 4.8 33.9 14.4 44.7 9.6 10.8 21 18.5 34 22.9 13.1 4.5 28.9 8.3 47.6 11.6 14.6 2.7 25.1 5.3 31.6 7.8s9.8 6.5 9.8 11.8c0 10.4-9.7 15.6-29.3 15.6-13.7 0-28.5-2.3-44.7-6.9-16.1-4.6-29.2-11.3-39.3-20.2l-33.3 60c9.2 7.4 24.6 14.7 46.2 22 21.7 7.3 45.2 10.9 70.7 10.9 34.7 0 62.9-7.4 84.5-22.4 21.7-15 32.5-37.3 32.5-66.9 0-19.3-5-34.2-15.1-44.9s-22-18.3-35.8-22.7zM2883.4 575.3c0-19.3-5-34.2-15.1-44.9s-22-18.3-35.8-22.7c-13.8-4.4-30.6-8.1-50.5-11.1-15.1-2.7-26.1-5.2-32.9-7.6-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7 6.7-10.9c4.4-2.2 11.5-3.3 21.3-3.3 11.6 0 24.3 2.4 38.1 7.2 13.9 4.8 26.2 11 36.9 18.4l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8-18.7-7.1-39.6-10.7-62.7-10.7-33.7 0-60.2 7.6-79.3 22.7-19.1 15.1-28.7 36.1-28.7 63.1 0 19 4.8 33.9 14.4 44.7 9.6 10.8 21 18.5 34 22.9 13.1 4.5 28.9 8.3 47.6 11.6 14.6 2.7 25.1 5.3 31.6 7.8s9.8 6.5 9.8 11.8c0 10.4-9.7 15.6-29.3 15.6-13.7 0-28.5-2.3-44.7-6.9-16.1-4.6-29.2-11.3-39.3-20.2l-33.3 60c9.2 7.4 24.6 14.7 46.2 22 21.7 7.3 45.2 10.9 70.7 10.9 34.7 0 62.9-7.4 84.5-22.4 21.7-15 32.5-37.3 32.5-66.9zM2460.7 738.7h59.6v17.2h-59.6zM2596.5 706.4c-5.7 0-11 1-15.8 3s-9 5-12.5 8.9v-9.4h-19.4v93.6h19.4v-52c0-8.6 2.1-15.3 6.3-20 4.2-4.7 9.5-7.1 15.9-7.1 7.8 0 13.4 2.3 16.8 6.7 3.4 4.5 5.1 11.3 5.1 20.5v52h19.4v-56.8c0-12.8-3.2-22.6-9.5-29.3-6.4-6.7-14.9-10.1-25.7-10.1zM2733.8 717.7c-3.6-3.4-7.9-6.1-13.1-8.2s-10.6-3.1-16.2-3.1c-8.7 0-16.5 2.1-23.5 6.3s-12.5 10-16.5 17.3c-4 7.3-6 15.4-6 24.4 0 8.9 2 17.1 6 24.3 4 7.3 9.5 13 16.5 17.2s14.9 6.3 23.5 6.3c5.6 0 11-1 16.2-3.1 5.1-2.1 9.5-4.8 13.1-8.2v24.4c0 8.5-2.5 14.8-7.6 18.7-5 3.9-11 5.9-18 5.9-6.7 0-12.4-1.6-17.3-4.7-4.8-3.1-7.6-7.7-8.3-13.8h-19.4c.6 7.7 2.9 14.2 7.1 19.5s9.6 9.3 16.2 12c6.6 2.7 13.8 4 21.7 4 12.8 0 23.5-3.4 32-10.1 8.6-6.7 12.8-17.1 12.8-31.1V708.9h-19.2v8.8zm-1.6 52.4c-2.5 4.7-6 8.3-10.4 11.2-4.4 2.7-9.4 4-14.9 4-5.7 0-10.8-1.4-15.2-4.3s-7.8-6.7-10.2-11.4c-2.3-4.8-3.5-9.8-3.5-15.2 0-5.5 1.1-10.6 3.5-15.3s5.8-8.5 10.2-11.3 9.5-4.2 15.2-4.2c5.5 0 10.5 1.4 14.9 4s7.9 6.3 10.4 11 3.8 10 3.8 15.8-1.3 11-3.8 15.7zM2867.9 708.9h-21.4l-25.6 33-25.4-33h-22.4l36 46.1-37.6 47.5h21.4l27.2-34.6 27.1 34.7h22.4l-37.6-48.2zM757.6 293.7c-20-10.8-42.6-16.2-67.8-16.2H600c-8.5 39.2-21.1 76.4-37.6 111.3-9.9 20.8-21.1 40.6-33.6 59.4v207.2h88.9V521.5h72c25.2 0 47.8-5.4 67.8-16.2s35.7-25.6 47.1-44.2c11.4-18.7 17.1-39.1 17.1-61.3.1-22.7-5.6-43.3-17-61.9-11.4-18.7-27.1-33.4-47.1-44.2zm-41 140.6c-9.3 8.9-21.6 13.3-36.7 13.3l-62.2.4v-92.5l62.2-.4c15.1 0 27.3 4.4 36.7 13.3 9.4 8.9 14 19.9 14 32.9 0 13.2-4.6 24.1-14 33z"/> | ||||||
| <svg version="1.1" |   <path d="M140 713.7c-3.4-16.4-10.3-49.1-11.2-49.1C-16.9 577.5.4 426.6 48.6 340.4 59 449 251.2 524 139.1 656.8c-.9 1.7 5.2 22.4 10.3 41.4 22.4-37.9 56-83.6 54.3-87.9C65.9 273.9 496.9 248.1 586.6 39.4c40.5 201.8-20.7 513.9-367.2 593.2-1.7.9-62.9 108.6-65.5 109.5 0-1.7-25.9-.9-22.4-9.5 1.6-5.2 5.1-12 8.5-18.9zm-4.3-81.1c44-50.9-7.8-137.9-38.8-166.4 52.6 90.5 49.1 143.1 38.8 166.4z" style="fill:#17541f"/> | ||||||
| 	 id="svg9580" inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)" sodipodi:docname="logo.svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" |  | ||||||
| 	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 2897.4 896.6" |  | ||||||
| 	 style="enable-background:new 0 0 2897.4 896.6;" xml:space="preserve"> |  | ||||||
| <style type="text/css"> |  | ||||||
| 	.st0{fill:#17541F;} |  | ||||||
| </style> |  | ||||||
| <sodipodi:namedview  bordercolor="#666666" borderopacity="1" gridtolerance="10" guidetolerance="10" id="namedview9582" inkscape:current-layer="g9578" inkscape:cx="1393.617" inkscape:cy="393.61704" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-height="1016" inkscape:window-maximized="1" inkscape:window-width="1920" inkscape:window-x="1280" inkscape:window-y="27" inkscape:zoom="0.46439736" objecttolerance="10" pagecolor="#ffffff" showgrid="false"> |  | ||||||
| 	</sodipodi:namedview> |  | ||||||
| <g> |  | ||||||
| 	<path d="M1022.3,428.7c-17.8-19.9-42.7-29.8-74.7-29.8c-22.3,0-42.4,5.7-60.5,17.3c-18.1,11.6-32.3,27.5-42.5,47.8 |  | ||||||
| 		s-15.3,42.9-15.3,67.8c0,24.9,5.1,47.5,15.3,67.8c10.3,20.3,24.4,36.2,42.5,47.8c18.1,11.5,38.3,17.3,60.5,17.3 |  | ||||||
| 		c32,0,56.9-9.9,74.7-29.8v20.4v0.2h84.5V408.3h-84.5V428.7z M1010.5,575c-10.2,11.7-23.6,17.6-40.2,17.6s-29.9-5.9-40-17.6 |  | ||||||
| 		s-15.1-26.1-15.1-43.3c0-17.1,5-31.6,15.1-43.3s23.4-17.6,40-17.6c16.6,0,30,5.9,40.2,17.6s15.3,26.1,15.3,43.3 |  | ||||||
| 		S1020.7,563.3,1010.5,575z"/> |  | ||||||
| 	<path d="M1381,416.1c-18.1-11.5-38.3-17.3-60.5-17.4c-32,0-56.9,9.9-74.7,29.8v-20.4h-84.5v390.7h84.5v-164 |  | ||||||
| 		c17.8,19.9,42.7,29.8,74.7,29.8c22.3,0,42.4-5.7,60.5-17.3s32.3-27.5,42.5-47.8c10.2-20.3,15.3-42.9,15.3-67.8s-5.1-47.5-15.3-67.8 |  | ||||||
| 		C1413.2,443.6,1399.1,427.7,1381,416.1z M1337.9,575c-10.1,11.7-23.4,17.6-40,17.6s-29.9-5.9-40-17.6s-15.1-26.1-15.1-43.3 |  | ||||||
| 		c0-17.1,5-31.6,15.1-43.3s23.4-17.6,40-17.6s29.9,5.9,40,17.6s15.1,26.1,15.1,43.3S1347.9,563.3,1337.9,575z"/> |  | ||||||
| 	<path d="M1672.2,416.8c-20.5-12-43-18-67.6-18c-24.9,0-47.6,5.9-68,17.6c-20.4,11.7-36.5,27.7-48.2,48s-17.6,42.7-17.6,67.3 |  | ||||||
| 		c0.3,25.2,6.2,47.8,17.8,68c11.5,20.2,28,36,49.3,47.6c21.3,11.5,45.9,17.3,73.8,17.3c48.6,0,86.8-14.7,114.7-44l-52.5-48.9 |  | ||||||
| 		c-8.6,8.3-17.6,14.6-26.7,19c-9.3,4.3-21.1,6.4-35.3,6.4c-11.6,0-22.5-3.6-32.7-10.9c-10.3-7.3-17.1-16.5-20.7-27.8h180l0.4-11.6 |  | ||||||
| 		c0-29.6-6-55.7-18-78.2S1692.6,428.8,1672.2,416.8z M1558.3,503.2c2.1-12.1,7.5-21.8,16.2-29.1s18.7-10.9,30-10.9 |  | ||||||
| 		s21.2,3.6,29.8,10.9c8.6,7.2,13.9,16.9,16,29.1H1558.3z"/> |  | ||||||
| 	<path d="M1895.3,411.7c-11,5.6-20.3,13.7-28,24.4h-0.1v-28h-84.5v247.3h84.5V536.3c0-22.6,4.7-38.1,14.2-46.5 |  | ||||||
| 		c9.5-8.5,22.7-12.7,39.6-12.7c6.2,0,13.5,1,21.8,3.1l10.7-72c-5.9-3.3-14.5-4.9-25.8-4.9C1917.1,403.3,1906.3,406.1,1895.3,411.7z" |  | ||||||
| 		/> |  | ||||||
| 	<rect x="1985" y="277.4" width="84.5" height="377.8"/> |  | ||||||
| 	<path d="M2313.2,416.8c-20.5-12-43-18-67.6-18c-24.9,0-47.6,5.9-68,17.6s-36.5,27.7-48.2,48c-11.7,20.3-17.6,42.7-17.6,67.3 |  | ||||||
| 		c0.3,25.2,6.2,47.8,17.8,68c11.5,20.2,28,36,49.3,47.6c21.3,11.5,45.9,17.3,73.8,17.3c48.6,0,86.8-14.7,114.7-44l-52.5-48.9 |  | ||||||
| 		c-8.6,8.3-17.6,14.6-26.7,19c-9.3,4.3-21.1,6.4-35.3,6.4c-11.6,0-22.5-3.6-32.7-10.9c-10.3-7.3-17.1-16.5-20.7-27.8h180l0.4-11.6 |  | ||||||
| 		c0-29.6-6-55.7-18-78.2S2333.6,428.8,2313.2,416.8z M2199.3,503.2c2.1-12.1,7.5-21.8,16.2-29.1s18.7-10.9,30-10.9 |  | ||||||
| 		s21.2,3.6,29.8,10.9c8.6,7.2,13.9,16.9,16,29.1H2199.3z"/> |  | ||||||
| 	<path d="M2583.6,507.7c-13.8-4.4-30.6-8.1-50.5-11.1c-15.1-2.7-26.1-5.2-32.9-7.6c-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7,6.7-10.9 |  | ||||||
| 		c4.4-2.2,11.5-3.3,21.3-3.3c11.6,0,24.3,2.4,38.1,7.2c13.9,4.8,26.2,11,36.9,18.4l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8 |  | ||||||
| 		c-18.7-7.1-39.6-10.7-62.7-10.7c-33.7,0-60.2,7.6-79.3,22.7c-19.1,15.1-28.7,36.1-28.7,63.1c0,19,4.8,33.9,14.4,44.7 |  | ||||||
| 		c9.6,10.8,21,18.5,34,22.9c13.1,4.5,28.9,8.3,47.6,11.6c14.6,2.7,25.1,5.3,31.6,7.8s9.8,6.5,9.8,11.8c0,10.4-9.7,15.6-29.3,15.6 |  | ||||||
| 		c-13.7,0-28.5-2.3-44.7-6.9c-16.1-4.6-29.2-11.3-39.3-20.2l-33.3,60c9.2,7.4,24.6,14.7,46.2,22c21.7,7.3,45.2,10.9,70.7,10.9 |  | ||||||
| 		c34.7,0,62.9-7.4,84.5-22.4c21.7-15,32.5-37.3,32.5-66.9c0-19.3-5-34.2-15.1-44.9S2597.4,512.1,2583.6,507.7z"/> |  | ||||||
| 	<path d="M2883.4,575.3c0-19.3-5-34.2-15.1-44.9s-22-18.3-35.8-22.7c-13.8-4.4-30.6-8.1-50.5-11.1c-15.1-2.7-26.1-5.2-32.9-7.6 |  | ||||||
| 		c-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7,6.7-10.9c4.4-2.2,11.5-3.3,21.3-3.3c11.6,0,24.3,2.4,38.1,7.2c13.9,4.8,26.2,11,36.9,18.4 |  | ||||||
| 		l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8c-18.7-7.1-39.6-10.7-62.7-10.7c-33.7,0-60.2,7.6-79.3,22.7 |  | ||||||
| 		c-19.1,15.1-28.7,36.1-28.7,63.1c0,19,4.8,33.9,14.4,44.7c9.6,10.8,21,18.5,34,22.9c13.1,4.5,28.9,8.3,47.6,11.6 |  | ||||||
| 		c14.6,2.7,25.1,5.3,31.6,7.8s9.8,6.5,9.8,11.8c0,10.4-9.7,15.6-29.3,15.6c-13.7,0-28.5-2.3-44.7-6.9c-16.1-4.6-29.2-11.3-39.3-20.2 |  | ||||||
| 		l-33.3,60c9.2,7.4,24.6,14.7,46.2,22c21.7,7.3,45.2,10.9,70.7,10.9c34.7,0,62.9-7.4,84.5-22.4 |  | ||||||
| 		C2872.6,627.2,2883.4,604.9,2883.4,575.3z"/> |  | ||||||
| 	<rect x="2460.7" y="738.7" width="59.6" height="17.2"/> |  | ||||||
| 	<path d="M2596.5,706.4c-5.7,0-11,1-15.8,3s-9,5-12.5,8.9v-9.4h-19.4v93.6h19.4v-52c0-8.6,2.1-15.3,6.3-20c4.2-4.7,9.5-7.1,15.9-7.1 |  | ||||||
| 		c7.8,0,13.4,2.3,16.8,6.7c3.4,4.5,5.1,11.3,5.1,20.5v52h19.4v-56.8c0-12.8-3.2-22.6-9.5-29.3 |  | ||||||
| 		C2615.8,709.8,2607.3,706.4,2596.5,706.4z"/> |  | ||||||
| 	<path d="M2733.8,717.7c-3.6-3.4-7.9-6.1-13.1-8.2s-10.6-3.1-16.2-3.1c-8.7,0-16.5,2.1-23.5,6.3s-12.5,10-16.5,17.3 |  | ||||||
| 		c-4,7.3-6,15.4-6,24.4c0,8.9,2,17.1,6,24.3c4,7.3,9.5,13,16.5,17.2s14.9,6.3,23.5,6.3c5.6,0,11-1,16.2-3.1 |  | ||||||
| 		c5.1-2.1,9.5-4.8,13.1-8.2v24.4c0,8.5-2.5,14.8-7.6,18.7c-5,3.9-11,5.9-18,5.9c-6.7,0-12.4-1.6-17.3-4.7c-4.8-3.1-7.6-7.7-8.3-13.8 |  | ||||||
| 		h-19.4c0.6,7.7,2.9,14.2,7.1,19.5s9.6,9.3,16.2,12c6.6,2.7,13.8,4,21.7,4c12.8,0,23.5-3.4,32-10.1c8.6-6.7,12.8-17.1,12.8-31.1 |  | ||||||
| 		V708.9h-19.2V717.7z M2732.2,770.1c-2.5,4.7-6,8.3-10.4,11.2c-4.4,2.7-9.4,4-14.9,4c-5.7,0-10.8-1.4-15.2-4.3s-7.8-6.7-10.2-11.4 |  | ||||||
| 		c-2.3-4.8-3.5-9.8-3.5-15.2c0-5.5,1.1-10.6,3.5-15.3s5.8-8.5,10.2-11.3s9.5-4.2,15.2-4.2c5.5,0,10.5,1.4,14.9,4s7.9,6.3,10.4,11 |  | ||||||
| 		s3.8,10,3.8,15.8S2734.7,765.4,2732.2,770.1z"/> |  | ||||||
| 	<polygon points="2867.9,708.9 2846.5,708.9 2820.9,741.9 2795.5,708.9 2773.1,708.9 2809.1,755 2771.5,802.5 2792.9,802.5  |  | ||||||
| 		2820.1,767.9 2847.2,802.6 2869.6,802.6 2832,754.4 	"/> |  | ||||||
| 	<path d="M757.6,293.7c-20-10.8-42.6-16.2-67.8-16.2H600c-8.5,39.2-21.1,76.4-37.6,111.3c-9.9,20.8-21.1,40.6-33.6,59.4v207.2h88.9 |  | ||||||
| 		V521.5h72c25.2,0,47.8-5.4,67.8-16.2s35.7-25.6,47.1-44.2c11.4-18.7,17.1-39.1,17.1-61.3c0.1-22.7-5.6-43.3-17-61.9 |  | ||||||
| 		C793.3,319.2,777.6,304.5,757.6,293.7z M716.6,434.3c-9.3,8.9-21.6,13.3-36.7,13.3l-62.2,0.4v-92.5l62.2-0.4 |  | ||||||
| 		c15.1,0,27.3,4.4,36.7,13.3c9.4,8.9,14,19.9,14,32.9C730.6,414.5,726,425.4,716.6,434.3z"/> |  | ||||||
| </g> |  | ||||||
| <path class="st0" d="M140,713.7c-3.4-16.4-10.3-49.1-11.2-49.1c-145.7-87.1-128.4-238-80.2-324.2C59,449,251.2,524,139.1,656.8 |  | ||||||
| 	c-0.9,1.7,5.2,22.4,10.3,41.4c22.4-37.9,56-83.6,54.3-87.9C65.9,273.9,496.9,248.1,586.6,39.4c40.5,201.8-20.7,513.9-367.2,593.2 |  | ||||||
| 	c-1.7,0.9-62.9,108.6-65.5,109.5c0-1.7-25.9-0.9-22.4-9.5C133.1,727.4,136.6,720.6,140,713.7L140,713.7z M135.7,632.6 |  | ||||||
| 	c44-50.9-7.8-137.9-38.8-166.4C149.5,556.7,146,609.3,135.7,632.6L135.7,632.6z"/> |  | ||||||
| </svg> | </svg> | ||||||
|   | |||||||
| Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 5.4 KiB | 
 phail
					phail