mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Merge pull request #695 from paperless-ngx/beta
[Beta] Paperless-ngx v1.7.0 Release Candidate 1
This commit is contained in:
		| @@ -17,3 +17,5 @@ | ||||
| **/htmlcov | ||||
| /src/.pytest_cache | ||||
| .idea | ||||
| .venv/ | ||||
| .vscode/ | ||||
|   | ||||
| @@ -18,14 +18,20 @@ max_line_length = off | ||||
| indent_size = 4 | ||||
| indent_style = space | ||||
|  | ||||
| [*.yml] | ||||
| [*.{yml,yaml}] | ||||
| indent_style = space | ||||
|  | ||||
| [*.rst] | ||||
| indent_style = space | ||||
|  | ||||
| [*.md] | ||||
| indent_style = space | ||||
|  | ||||
| # Tests don't get a line width restriction.  It's still a good idea to follow | ||||
| # the 79 character rule, but in the interests of clarity, tests often need to | ||||
| # violate it. | ||||
| [**/test_*.py] | ||||
| max_line_length = off | ||||
|  | ||||
| [Dockerfile] | ||||
| indent_style = space | ||||
|   | ||||
							
								
								
									
										2
									
								
								.env
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								.env
									
									
									
									
									
								
							| @@ -1,2 +1,2 @@ | ||||
| COMPOSE_PROJECT_NAME=paperless | ||||
| export PROMPT="(pipenv-projectname)$P$G" | ||||
| export PROMPT="(pipenv-projectname)$P$G" | ||||
|   | ||||
							
								
								
									
										15
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							| @@ -8,7 +8,7 @@ updates: | ||||
|     target-branch: "dev" | ||||
|     # Look for `package.json` and `lock` files in the `root` directory | ||||
|     directory: "/src-ui" | ||||
|     # Check the npm registry for updates every week | ||||
|     # Check the npm registry for updates every month | ||||
|     schedule: | ||||
|       interval: "monthly" | ||||
|     # Add reviewers | ||||
| @@ -23,6 +23,19 @@ updates: | ||||
|     # Check for updates once a week | ||||
|     schedule: | ||||
|       interval: "weekly" | ||||
|     labels: | ||||
|       - "backend" | ||||
|       - "dependencies" | ||||
|  | ||||
|   # Enable updates for Github Actions | ||||
|   - package-ecosystem: "github-actions" | ||||
|     directory: "/" | ||||
|     schedule: | ||||
|       # Check for updates to GitHub Actions every month | ||||
|       interval: "monthly" | ||||
|     labels: | ||||
|       - "ci-cd" | ||||
|       - "dependencies" | ||||
|     # Add reviewers | ||||
|     reviewers: | ||||
|       - "paperless-ngx/backend" | ||||
|   | ||||
							
								
								
									
										252
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										252
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -13,90 +13,100 @@ on: | ||||
|  | ||||
| jobs: | ||||
|   documentation: | ||||
|     name: "Build Documentation" | ||||
|     runs-on: ubuntu-20.04 | ||||
|     steps: | ||||
|       - | ||||
|         name: Checkout | ||||
|         uses: actions/checkout@v2 | ||||
|         uses: actions/checkout@v3 | ||||
|       - | ||||
|         name: Install pipenv | ||||
|         run: pipx install pipenv | ||||
|       - | ||||
|         name: Set up Python | ||||
|         uses: actions/setup-python@v2 | ||||
|         uses: actions/setup-python@v3 | ||||
|         with: | ||||
|           python-version: 3.9 | ||||
|       - | ||||
|         name: Get pip cache dir | ||||
|         id: pip-cache | ||||
|         run: | | ||||
|           echo "::set-output name=dir::$(pip cache dir)" | ||||
|       - | ||||
|         name: Persistent Github pip cache | ||||
|         uses: actions/cache@v2 | ||||
|         with: | ||||
|           path: ${{ steps.pip-cache.outputs.dir }} | ||||
|           key: ${{ runner.os }}-pip3.8} | ||||
|           cache: "pipenv" | ||||
|           cache-dependency-path: 'Pipfile.lock' | ||||
|       - | ||||
|         name: Install dependencies | ||||
|         run: | | ||||
|           pip install --upgrade pipenv | ||||
|           pipenv install --system --dev --ignore-pipfile | ||||
|           pipenv sync --dev | ||||
|       - | ||||
|         name: Make documentation | ||||
|         run: | | ||||
|           cd docs/ | ||||
|           make html | ||||
|           pipenv run make html | ||||
|       - | ||||
|         name: Upload artifact | ||||
|         uses: actions/upload-artifact@v2 | ||||
|         uses: actions/upload-artifact@v3 | ||||
|         with: | ||||
|           name: documentation | ||||
|           path: docs/_build/html/ | ||||
|  | ||||
|   codestyle: | ||||
|   code-checks-backend: | ||||
|     name: "Backend Code Checks" | ||||
|     runs-on: ubuntu-20.04 | ||||
|     steps: | ||||
|       - | ||||
|         name: Checkout | ||||
|         uses: actions/checkout@v2 | ||||
|         uses: actions/checkout@v3 | ||||
|       - | ||||
|         name: Set up Python | ||||
|         uses: actions/setup-python@v2 | ||||
|         with: | ||||
|           python-version: 3.9 | ||||
|       - | ||||
|         name: Get pip cache dir | ||||
|         id: pip-cache | ||||
|         name: Install checkers | ||||
|         run: | | ||||
|           echo "::set-output name=dir::$(pip cache dir)" | ||||
|           pipx install reorder-python-imports | ||||
|           pipx install yesqa | ||||
|           pipx install add-trailing-comma | ||||
|           pipx install flake8 | ||||
|       - | ||||
|         name: Persistent Github pip cache | ||||
|         uses: actions/cache@v2 | ||||
|         with: | ||||
|           path: ${{ steps.pip-cache.outputs.dir }} | ||||
|           key: ${{ runner.os }}-pip${{ matrix.python-version }} | ||||
|       - | ||||
|         name: Install dependencies | ||||
|         name: Run reorder-python-imports | ||||
|         run: | | ||||
|           pip install --upgrade pipenv | ||||
|           pipenv install --system --dev --ignore-pipfile | ||||
|           find src/ -type f -name '*.py' ! -path "*/migrations/*" | xargs reorder-python-imports | ||||
|       - | ||||
|         name: Codestyle | ||||
|         name: Run yesqa | ||||
|         run: | | ||||
|           cd src/ | ||||
|           pycodestyle --max-line-length=88 --ignore=E121,E123,E126,E226,E24,E704,W503,W504,E203 | ||||
|   codeformatting: | ||||
|     runs-on: ubuntu-20.04 | ||||
|     steps: | ||||
|           find src/ -type f -name '*.py' ! -path "*/migrations/*" | xargs yesqa | ||||
|       - | ||||
|         name: Checkout | ||||
|         uses: actions/checkout@v2 | ||||
|         name: Run add-trailing-comma | ||||
|         run: | | ||||
|           find src/ -type f -name '*.py' ! -path "*/migrations/*" | xargs add-trailing-comma | ||||
|       # black is placed after add-trailing-comma because it may format differently | ||||
|       # if a trailing comma is added | ||||
|       - | ||||
|         name: Run black | ||||
|         uses: psf/black@stable | ||||
|         with: | ||||
|           options: "--check --diff" | ||||
|           version: "22.3.0" | ||||
|       - | ||||
|         name: Run flake8 checks | ||||
|         run: | | ||||
|           cd src/ | ||||
|           flake8 --max-line-length=88 --ignore=E203,W503 | ||||
|  | ||||
|   tests: | ||||
|   code-checks-frontend: | ||||
|     name: "Frontend Code Checks" | ||||
|     runs-on: ubuntu-20.04 | ||||
|     steps: | ||||
|       - | ||||
|         name: Checkout | ||||
|         uses: actions/checkout@v3 | ||||
|       - uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: '16' | ||||
|       - | ||||
|         name: Install prettier | ||||
|         run: | | ||||
|           npm install prettier | ||||
|       - | ||||
|         name: Run prettier | ||||
|         run: | ||||
|           npx prettier --check --ignore-path Pipfile.lock **/*.js **/*.ts *.md **/*.md | ||||
|  | ||||
|   tests-backend: | ||||
|     needs: [code-checks-backend] | ||||
|     name: "Backend Tests (${{ matrix.python-version }})" | ||||
|     runs-on: ubuntu-20.04 | ||||
|     strategy: | ||||
|       matrix: | ||||
| @@ -105,73 +115,94 @@ jobs: | ||||
|     steps: | ||||
|       - | ||||
|         name: Checkout | ||||
|         uses: actions/checkout@v2 | ||||
|         uses: actions/checkout@v3 | ||||
|         with: | ||||
|           fetch-depth: 2 | ||||
|       - | ||||
|         name: Install pipenv | ||||
|         run: pipx install pipenv | ||||
|       - | ||||
|         name: Set up Python | ||||
|         uses: actions/setup-python@v2 | ||||
|         uses: actions/setup-python@v3 | ||||
|         with: | ||||
|           python-version: "${{ matrix.python-version }}" | ||||
|           cache: "pipenv" | ||||
|           cache-dependency-path: 'Pipfile.lock' | ||||
|       - | ||||
|         name: Get pip cache dir | ||||
|         id: pip-cache | ||||
|         run: | | ||||
|           echo "::set-output name=dir::$(pip cache dir)" | ||||
|       - | ||||
|         name: Persistent Github pip cache | ||||
|         uses: actions/cache@v2 | ||||
|         with: | ||||
|           path: ${{ steps.pip-cache.outputs.dir }} | ||||
|           key: ${{ runner.os }}-pip${{ matrix.python-version }} | ||||
|       - | ||||
|         name: Install dependencies | ||||
|         name: Install system dependencies | ||||
|         run: | | ||||
|           sudo apt-get update -qq | ||||
|           sudo apt-get install -qq --no-install-recommends unpaper tesseract-ocr imagemagick ghostscript optipng | ||||
|           pip install --upgrade pipenv | ||||
|           pipenv install --system --dev --ignore-pipfile | ||||
|           sudo apt-get install -qq --no-install-recommends unpaper tesseract-ocr imagemagick ghostscript optipng libzbar0 poppler-utils | ||||
|       - | ||||
|         name: Install Python dependencies | ||||
|         run: | | ||||
|           pipenv sync --dev | ||||
|       - | ||||
|         name: Tests | ||||
|         run: | | ||||
|           cd src/ | ||||
|           pytest | ||||
|           pipenv run pytest | ||||
|       - | ||||
|         name: Get changed files | ||||
|         id: changed-files-specific | ||||
|         uses: tj-actions/changed-files@v18.1 | ||||
|         with: | ||||
|           files: | | ||||
|             src/** | ||||
|       - | ||||
|         name: List all changed files | ||||
|         run: | | ||||
|           for file in ${{ steps.changed-files-specific.outputs.all_changed_files }}; do | ||||
|             echo "${file} was changed" | ||||
|           done | ||||
|       - | ||||
|         name: Publish coverage results | ||||
|         if: matrix.python-version == '3.9' | ||||
|         if: matrix.python-version == '3.9' && steps.changed-files-specific.outputs.any_changed == 'true' | ||||
|         env: | ||||
|           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||
|         # https://github.com/coveralls-clients/coveralls-python/issues/251 | ||||
|         run: | | ||||
|           cd src/ | ||||
|           coveralls --service=github | ||||
|           pipenv run coveralls --service=github | ||||
|  | ||||
|   tests-frontend: | ||||
|     needs: [code-checks-frontend] | ||||
|     name: "Frontend Tests" | ||||
|     runs-on: ubuntu-20.04 | ||||
|     strategy: | ||||
|       matrix: | ||||
|         node-version: [16.x] | ||||
|     steps: | ||||
|       - uses: actions/checkout@v3 | ||||
|       - name: Use Node.js ${{ matrix.node-version }} | ||||
|         uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: ${{ matrix.node-version }} | ||||
|       - run: cd src-ui && npm ci | ||||
|       - run: cd src-ui && npm run test | ||||
|       - run: cd src-ui && npm run e2e:ci | ||||
|  | ||||
|   # build and push image to docker hub. | ||||
|   build-docker-image: | ||||
|     if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature-') || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/beta' || startsWith(github.ref, 'refs/tags/ngx-') || startsWith(github.ref, 'refs/tags/beta-')) | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: [tests, codeformatting, codestyle] | ||||
|     concurrency: | ||||
|       group: ${{ github.workflow }}-build-docker-image-${{ github.ref }} | ||||
|       cancel-in-progress: true | ||||
|     runs-on: ubuntu-20.04 | ||||
|     needs: [tests-backend, tests-frontend] | ||||
|     steps: | ||||
|       - | ||||
|         name: Prepare | ||||
|         id: prepare | ||||
|         run: | | ||||
|           IMAGE_NAME=ghcr.io/${{ github.repository }} | ||||
|           if [[ $GITHUB_REF == refs/tags/ngx-* ]]; then | ||||
|             TAGS=${IMAGE_NAME}:${GITHUB_REF#refs/tags/ngx-},${IMAGE_NAME}:latest | ||||
|             INSPECT_TAG=${IMAGE_NAME}:latest | ||||
|           elif [[ $GITHUB_REF == refs/tags/beta-* ]]; then | ||||
|             TAGS=${IMAGE_NAME}:beta | ||||
|             INSPECT_TAG=${TAGS} | ||||
|           elif [[ $GITHUB_REF == refs/heads/* ]]; then | ||||
|             TAGS=${IMAGE_NAME}:${GITHUB_REF#refs/heads/} | ||||
|             INSPECT_TAG=${TAGS} | ||||
|           else | ||||
|             exit 1 | ||||
|           fi | ||||
|           echo ::set-output name=tags::${TAGS} | ||||
|           echo ::set-output name=inspect_tag::${INSPECT_TAG} | ||||
|         name: Gather Docker metadata | ||||
|         id: docker-meta | ||||
|         uses: docker/metadata-action@v3 | ||||
|         with: | ||||
|           images: ghcr.io/${{ github.repository }} | ||||
|           tags: | | ||||
|             type=ref,event=branch | ||||
|             type=ref,event=tag | ||||
|       - | ||||
|         name: Checkout | ||||
|         uses: actions/checkout@v2 | ||||
|         uses: actions/checkout@v3 | ||||
|       - | ||||
|         name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v1 | ||||
| @@ -192,36 +223,37 @@ jobs: | ||||
|           context: . | ||||
|           file: ./Dockerfile | ||||
|           platforms: linux/amd64,linux/arm/v7,linux/arm64 | ||||
|           push: true | ||||
|           tags: ${{ steps.prepare.outputs.tags }} | ||||
|           push: ${{ github.event_name != 'pull_request' }} | ||||
|           tags: ${{ steps.docker-meta.outputs.tags }} | ||||
|           labels: ${{ steps.docker-meta.outputs.labels }} | ||||
|           cache-from: type=gha | ||||
|           cache-to: type=gha,mode=max | ||||
|       - | ||||
|         name: Inspect image | ||||
|         run: | | ||||
|           docker buildx imagetools inspect ${{ steps.prepare.outputs.inspect_tag }} | ||||
|           docker buildx imagetools inspect ${{ fromJSON(steps.docker-meta.outputs.json).tags[0] }} | ||||
|       - | ||||
|         name: Export frontend artifact from docker | ||||
|         run: | | ||||
|           docker run -d --name frontend-extract ${{ steps.prepare.outputs.inspect_tag }} | ||||
|           docker create --name frontend-extract ${{ fromJSON(steps.docker-meta.outputs.json).tags[0] }} | ||||
|           docker cp frontend-extract:/usr/src/paperless/src/documents/static/frontend src/documents/static/frontend/ | ||||
|       - | ||||
|         name: Upload frontend artifact | ||||
|         uses: actions/upload-artifact@v2 | ||||
|         uses: actions/upload-artifact@v3 | ||||
|         with: | ||||
|           name: frontend-compiled | ||||
|           path: src/documents/static/frontend/ | ||||
|  | ||||
|   build-release: | ||||
|     needs: [build-docker-image, documentation, tests, codeformatting, codestyle] | ||||
|     needs: [build-docker-image, documentation] | ||||
|     runs-on: ubuntu-20.04 | ||||
|     steps: | ||||
|       - | ||||
|         name: Checkout | ||||
|         uses: actions/checkout@v2 | ||||
|         uses: actions/checkout@v3 | ||||
|       - | ||||
|         name: Set up Python | ||||
|         uses: actions/setup-python@v2 | ||||
|         uses: actions/setup-python@v3 | ||||
|         with: | ||||
|           python-version: 3.9 | ||||
|       - | ||||
| @@ -233,13 +265,13 @@ jobs: | ||||
|           pip3 install -r requirements.txt | ||||
|       - | ||||
|         name: Download frontend artifact | ||||
|         uses: actions/download-artifact@v2 | ||||
|         uses: actions/download-artifact@v3 | ||||
|         with: | ||||
|           name: frontend-compiled | ||||
|           path: src/documents/static/frontend/ | ||||
|       - | ||||
|         name: Download documentation artifact | ||||
|         uses: actions/download-artifact@v2 | ||||
|         uses: actions/download-artifact@v3 | ||||
|         with: | ||||
|           name: documentation | ||||
|           path: docs/_build/html/ | ||||
| @@ -274,19 +306,19 @@ jobs: | ||||
|           tar -cJf paperless-ngx.tar.xz paperless-ngx/ | ||||
|       - | ||||
|         name: Upload release artifact | ||||
|         uses: actions/upload-artifact@v2 | ||||
|         uses: actions/upload-artifact@v3 | ||||
|         with: | ||||
|           name: release | ||||
|           path: dist/paperless-ngx.tar.xz | ||||
|  | ||||
|   publish-release: | ||||
|     runs-on: ubuntu-latest | ||||
|     runs-on: ubuntu-20.04 | ||||
|     needs: build-release | ||||
|     if: contains(github.ref, 'refs/tags/ngx-') || contains(github.ref, 'refs/tags/beta-') | ||||
|     steps: | ||||
|       - | ||||
|         name: Download release artifact | ||||
|         uses: actions/download-artifact@v2 | ||||
|         uses: actions/download-artifact@v3 | ||||
|         with: | ||||
|           name: release | ||||
|           path: ./ | ||||
| @@ -297,24 +329,22 @@ jobs: | ||||
|           if [[ $GITHUB_REF == refs/tags/ngx-* ]]; then | ||||
|             echo ::set-output name=version::${GITHUB_REF#refs/tags/ngx-} | ||||
|             echo ::set-output name=prerelease::false | ||||
|             echo ::set-output name=body::"For a complete list of changes, see the changelog at https://paperless-ngx.readthedocs.io/en/latest/changelog.html" | ||||
|           elif [[ $GITHUB_REF == refs/tags/beta-* ]]; then | ||||
|             echo ::set-output name=version::${GITHUB_REF#refs/tags/beta-} | ||||
|             echo ::set-output name=prerelease::true | ||||
|             echo ::set-output name=body::"For a complete list of changes, see the changelog at https://github.com/paperless-ngx/paperless-ngx/blob/beta/docs/changelog.rst" | ||||
|           fi | ||||
|       - | ||||
|         name: Create release | ||||
|         id: create_release | ||||
|         uses: actions/create-release@v1 | ||||
|         name: Create Release and Changelog | ||||
|         id: create-release | ||||
|         uses: release-drafter/release-drafter@v5 | ||||
|         with: | ||||
|           name: Paperless-ngx ${{ steps.get_version.outputs.version }} | ||||
|           tag: ngx-${{ steps.get_version.outputs.version }} | ||||
|           version: ${{ steps.get_version.outputs.version }} | ||||
|           prerelease: ${{ steps.get_version.outputs.prerelease }} | ||||
|           publish: true # ensures release is not marked as draft | ||||
|         env: | ||||
|           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||
|         with: | ||||
|           tag_name: ngx-${{ steps.get_version.outputs.version }} | ||||
|           release_name: Paperless-ngx ${{ steps.get_version.outputs.version }} | ||||
|           draft: false | ||||
|           prerelease: ${{ steps.get_version.outputs.prerelease }} | ||||
|           body: ${{ steps.get_version.outputs.body }} | ||||
|       - | ||||
|         name: Upload release archive | ||||
|         id: upload-release-asset | ||||
| @@ -322,7 +352,7 @@ jobs: | ||||
|         env: | ||||
|           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||
|         with: | ||||
|           upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps | ||||
|           upload_url: ${{ steps.create-release.outputs.upload_url }} | ||||
|           asset_path: ./paperless-ngx.tar.xz | ||||
|           asset_name: paperless-ngx-${{ steps.get_version.outputs.version }}.tar.xz | ||||
|           asset_content_type: application/x-xz | ||||
|   | ||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -61,6 +61,9 @@ target/ | ||||
| # PyCharm | ||||
| .idea | ||||
|  | ||||
| # VS Code | ||||
| .vscode | ||||
|  | ||||
| # Other stuff that doesn't belong | ||||
| .virtualenv | ||||
| virtualenv | ||||
|   | ||||
							
								
								
									
										4
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| # https://prettier.io/docs/en/options.html#semicolons | ||||
| semi: false | ||||
| # https://prettier.io/docs/en/options.html#quotes | ||||
| singleQuote: true | ||||
							
								
								
									
										10
									
								
								CODEOWNERS
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								CODEOWNERS
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| /.github/workflows/ @paperless-ngx/ci-cd | ||||
| /docker/ @paperless-ngx/ci-cd | ||||
| /scripts/ @paperless-ngx/ci-cd | ||||
|  | ||||
| /src-ui/ @paperless-ngx/frontend | ||||
|  | ||||
| /src/ @paperless-ngx/backend | ||||
| Pipfile* @paperless-ngx/backend | ||||
| *.py @paperless-ngx/backend | ||||
| requirements.txt @paperless-ngx/backend | ||||
| @@ -17,23 +17,23 @@ diverse, inclusive, and healthy community. | ||||
| Examples of behavior that contributes to a positive environment for our | ||||
| community include: | ||||
|  | ||||
| * Demonstrating empathy and kindness toward other people | ||||
| * Being respectful of differing opinions, viewpoints, and experiences | ||||
| * Giving and gracefully accepting constructive feedback | ||||
| * Accepting responsibility and apologizing to those affected by our mistakes, | ||||
| - Demonstrating empathy and kindness toward other people | ||||
| - Being respectful of differing opinions, viewpoints, and experiences | ||||
| - Giving and gracefully accepting constructive feedback | ||||
| - Accepting responsibility and apologizing to those affected by our mistakes, | ||||
|   and learning from the experience | ||||
| * Focusing on what is best not just for us as individuals, but for the | ||||
| - Focusing on what is best not just for us as individuals, but for the | ||||
|   overall community | ||||
|  | ||||
| Examples of unacceptable behavior include: | ||||
|  | ||||
| * The use of sexualized language or imagery, and sexual attention or | ||||
| - The use of sexualized language or imagery, and sexual attention or | ||||
|   advances of any kind | ||||
| * Trolling, insulting or derogatory comments, and personal or political attacks | ||||
| * Public or private harassment | ||||
| * Publishing others' private information, such as a physical or email | ||||
| - Trolling, insulting or derogatory comments, and personal or political attacks | ||||
| - Public or private harassment | ||||
| - Publishing others' private information, such as a physical or email | ||||
|   address, without their explicit permission | ||||
| * Other conduct which could reasonably be considered inappropriate in a | ||||
| - Other conduct which could reasonably be considered inappropriate in a | ||||
|   professional setting | ||||
|  | ||||
| ## Enforcement Responsibilities | ||||
| @@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban. | ||||
| ### 4. Permanent Ban | ||||
|  | ||||
| **Community Impact**: Demonstrating a pattern of violation of community | ||||
| standards, including sustained inappropriate behavior,  harassment of an | ||||
| standards, including sustained inappropriate behavior, harassment of an | ||||
| individual, or aggression toward or disparagement of classes of individuals. | ||||
|  | ||||
| **Consequence**: A permanent ban from any sort of public interaction within | ||||
|   | ||||
| @@ -4,10 +4,10 @@ If you feel like contributing to the project, please do! Bug fixes and improveme | ||||
|  | ||||
| If you want to implement something big: | ||||
|  | ||||
| * Please start a discussion about that in the issues! Maybe something similar is already in development and we can make it happen together. | ||||
| * When making additions to the project, consider if the majority of users will benefit from your change. If not, you're probably better of forking the project. | ||||
| * Also consider if your change will get in the way of other users. A good change is a change that enhances the experience of some users who want that change and does not affect users who do not care about the change. | ||||
| * Please see the [paperless-ngx merge process](#merging-prs) below. | ||||
| - Please start a discussion about that in the issues! Maybe something similar is already in development and we can make it happen together. | ||||
| - When making additions to the project, consider if the majority of users will benefit from your change. If not, you're probably better of forking the project. | ||||
| - Also consider if your change will get in the way of other users. A good change is a change that enhances the experience of some users who want that change and does not affect users who do not care about the change. | ||||
| - Please see the [paperless-ngx merge process](#merging-prs) below. | ||||
|  | ||||
| ## Python | ||||
|  | ||||
| @@ -27,6 +27,8 @@ Please format and test your code! I know it's a hassle, but it makes sure that y | ||||
|  | ||||
| To test your code, execute `pytest` in the src/ directory. This also generates a html coverage report, which you can use to see if you missed anything important during testing. | ||||
|  | ||||
| Before you can run `pytest`, ensure to [properly set up your local environment](https://paperless-ngx.readthedocs.io/en/latest/extending.html#initial-setup-and-first-start). | ||||
|  | ||||
| ## More info: | ||||
|  | ||||
| ... is available in the documentation. https://paperless-ngx.readthedocs.io/en/latest/extending.html | ||||
| @@ -41,9 +43,9 @@ PRs deemed `non-trivial` will go through a stricter review process before being | ||||
|  | ||||
| Examples of `non-trivial` PRs might include: | ||||
|  | ||||
| * Additional features | ||||
| * Large changes to many distinct files | ||||
| * Breaking or depreciation of existing features | ||||
| - Additional features | ||||
| - Large changes to many distinct files | ||||
| - Breaking or depreciation of existing features | ||||
|  | ||||
| Our community review process for `non-trivial` PRs is the following: | ||||
|  | ||||
| @@ -75,18 +77,18 @@ If a language has already been added, and you would like to contribute new trans | ||||
| If you would like the project to be translated to another language, first head over to https://crwd.in/paperless-ngx to check if that language has already been enabled for translation. | ||||
| If not, please request the language to be added by creating an issue on GitHub. The issue should contain: | ||||
|  | ||||
| * English name of the language (the localized name can be added on Crowdin). | ||||
| * ISO language code. A list of those can be found here: https://support.crowdin.com/enterprise/language-codes/ | ||||
| * Date format commonly used for the language, e.g. dd/mm/yyyy, mm/dd/yyyy, etc. | ||||
| - English name of the language (the localized name can be added on Crowdin). | ||||
| - ISO language code. A list of those can be found here: https://support.crowdin.com/enterprise/language-codes/ | ||||
| - Date format commonly used for the language, e.g. dd/mm/yyyy, mm/dd/yyyy, etc. | ||||
|  | ||||
| After the language has been added and some translations have been made on Crowdin, the language needs to be enabled in the code. | ||||
| Note that there is no need to manually add a .po of .xlf file as those will be automatically generated and imported from Crowdin. | ||||
| The following files need to be changed: | ||||
|  | ||||
| * src-ui/angular.json (under the _projects/paperless-ui/i18n/locales_ JSON key) | ||||
| * src/paperless/settings.py (in the _LANGUAGES_ array) | ||||
| * src-ui/src/app/services/settings.service.ts (inside the _getLanguageOptions_ method) | ||||
| * src-ui/src/app/app.module.ts (import locale from _angular/common/locales_ and call _registerLocaleData_) | ||||
| - src-ui/angular.json (under the _projects/paperless-ui/i18n/locales_ JSON key) | ||||
| - src/paperless/settings.py (in the _LANGUAGES_ array) | ||||
| - src-ui/src/app/services/settings.service.ts (inside the _getLanguageOptions_ method) | ||||
| - src-ui/src/app/app.module.ts (import locale from _angular/common/locales_ and call _registerLocaleData_) | ||||
|  | ||||
| Please add the language in the correct order, alphabetically by locale. | ||||
| Note that _en-us_ needs to stay on top of the list, as it is the default project language | ||||
| @@ -102,26 +104,26 @@ Paperless-ngx is a community project. We do our best to delegate permission and | ||||
|  | ||||
| As of writing, there are 21 members in paperless-ngx. 4 of these people have complete administrative privileges to the repo: | ||||
|  | ||||
| * [@shamoon](https://github.com/shamoon) | ||||
| * [@bauerj](https://github.com/bauerj) | ||||
| * [@qcasey](https://github.com/qcasey) | ||||
| * [@FrankStrieter](https://github.com/FrankStrieter) | ||||
| - [@shamoon](https://github.com/shamoon) | ||||
| - [@bauerj](https://github.com/bauerj) | ||||
| - [@qcasey](https://github.com/qcasey) | ||||
| - [@FrankStrieter](https://github.com/FrankStrieter) | ||||
|  | ||||
| There are 5 teams collaborating on specific tasks within paperless-ngx: | ||||
|  | ||||
| * @paperless-ngx/backend (Python / django) | ||||
| * @paperless-ngx/frontend (JavaScript / Typescript) | ||||
| * @paperless-ngx/ci-cd (GitHub Actions / Deployment) | ||||
| * @paperless-ngx/issues (Issue triage) | ||||
| * @paperless-ngx/test (General testing for larger PRs) | ||||
| - @paperless-ngx/backend (Python / django) | ||||
| - @paperless-ngx/frontend (JavaScript / Typescript) | ||||
| - @paperless-ngx/ci-cd (GitHub Actions / Deployment) | ||||
| - @paperless-ngx/issues (Issue triage) | ||||
| - @paperless-ngx/test (General testing for larger PRs) | ||||
|  | ||||
| ## Permissions | ||||
|  | ||||
| All team members are notified when mentioned or assigned to a relevant issue or pull request. Additionally, each team has slightly different access to paperless-ngx: | ||||
|  | ||||
| * The **test** team has no special permissions. | ||||
| * The **issues** team has `triage` access. This means they can organize issues and pull requests. | ||||
| * The **backend**, **frontend**, and **ci-cd** teams have `write` access. This means they can approve PRs and push code, containers, releases, and more. | ||||
| - The **test** team has no special permissions. | ||||
| - The **issues** team has `triage` access. This means they can organize issues and pull requests. | ||||
| - The **backend**, **frontend**, and **ci-cd** teams have `write` access. This means they can approve PRs and push code, containers, releases, and more. | ||||
|  | ||||
| ## Joining | ||||
|  | ||||
|   | ||||
							
								
								
									
										140
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										140
									
								
								Dockerfile
									
									
									
									
									
								
							| @@ -3,64 +3,16 @@ FROM node:16 AS compile-frontend | ||||
| COPY . /src | ||||
|  | ||||
| WORKDIR /src/src-ui | ||||
| RUN npm update npm -g && npm install | ||||
| RUN npm update npm -g && npm ci --no-optional | ||||
| RUN ./node_modules/.bin/ng build --configuration production | ||||
|  | ||||
| FROM ghcr.io/paperless-ngx/builder/ngx-base:1.7.0 as main-app | ||||
|  | ||||
| FROM ubuntu:20.04 AS jbig2enc | ||||
|  | ||||
| WORKDIR /usr/src/jbig2enc | ||||
|  | ||||
| RUN apt-get update && apt-get install -y --no-install-recommends build-essential automake libtool libleptonica-dev zlib1g-dev git ca-certificates | ||||
|  | ||||
| RUN git clone https://github.com/agl/jbig2enc . | ||||
| RUN ./autogen.sh | ||||
| RUN ./configure && make | ||||
|  | ||||
|  | ||||
| FROM python:3.9-slim-bullseye | ||||
|  | ||||
| # Binary dependencies | ||||
| RUN apt-get update \ | ||||
| 	&& apt-get -y --no-install-recommends install \ | ||||
|   	# Basic dependencies | ||||
| 		curl \ | ||||
| 		gnupg \ | ||||
| 		imagemagick \ | ||||
| 		gettext \ | ||||
| 		tzdata \ | ||||
| 		gosu \ | ||||
| 		# fonts for text file thumbnail generation | ||||
| 		fonts-liberation \ | ||||
| 		# for Numpy | ||||
| 		libatlas-base-dev \ | ||||
| 		libxslt1-dev \ | ||||
| 		# thumbnail size reduction | ||||
| 		optipng \ | ||||
| 		libxml2 \ | ||||
| 		pngquant \ | ||||
| 		unpaper \ | ||||
| 		zlib1g \ | ||||
| 		ghostscript \ | ||||
| 		icc-profiles-free \ | ||||
|   	# Mime type detection | ||||
| 		file \ | ||||
| 		libmagic-dev \ | ||||
| 		media-types \ | ||||
| 		# OCRmyPDF dependencies | ||||
| 		liblept5 \ | ||||
| 		tesseract-ocr \ | ||||
| 		tesseract-ocr-eng \ | ||||
| 		tesseract-ocr-deu \ | ||||
| 		tesseract-ocr-fra \ | ||||
| 		tesseract-ocr-ita \ | ||||
| 		tesseract-ocr-spa \ | ||||
|   && rm -rf /var/lib/apt/lists/* | ||||
|  | ||||
| # copy jbig2enc | ||||
| COPY --from=jbig2enc /usr/src/jbig2enc/src/.libs/libjbig2enc* /usr/local/lib/ | ||||
| COPY --from=jbig2enc /usr/src/jbig2enc/src/jbig2 /usr/local/bin/ | ||||
| COPY --from=jbig2enc /usr/src/jbig2enc/src/*.h /usr/local/include/ | ||||
| LABEL org.opencontainers.image.authors="paperless-ngx team <hello@paperless-ngx.com>" | ||||
| LABEL org.opencontainers.image.documentation="https://paperless-ngx.readthedocs.io/en/latest/" | ||||
| LABEL org.opencontainers.image.source="https://github.com/paperless-ngx/paperless-ngx" | ||||
| LABEL org.opencontainers.image.url="https://github.com/paperless-ngx/paperless-ngx" | ||||
| LABEL org.opencontainers.image.licenses="GPL-3.0-only" | ||||
|  | ||||
| WORKDIR /usr/src/paperless/src/ | ||||
|  | ||||
| @@ -68,47 +20,31 @@ COPY requirements.txt ../ | ||||
|  | ||||
| # Python dependencies | ||||
| RUN apt-get update \ | ||||
|   # python-Levenshtein still needs to be compiled here | ||||
|   && apt-get -y --no-install-recommends install \ | ||||
| 		build-essential \ | ||||
| 		libpq-dev \ | ||||
| 		git \ | ||||
| 		zlib1g-dev \ | ||||
| 		libjpeg62-turbo-dev \ | ||||
| 	&& if [ "$(uname -m)" = "armv7l" ] || [ "$(uname -m)" = "aarch64" ]; \ | ||||
| 	  then echo "Building qpdf" \ | ||||
| 	  && mkdir -p /usr/src/qpdf \ | ||||
| 	  && cd /usr/src/qpdf \ | ||||
| 	  && git clone https://github.com/qpdf/qpdf.git . \ | ||||
| 	  && git checkout --quiet release-qpdf-10.6.2 \ | ||||
| 	  && ./configure \ | ||||
| 	  && make \ | ||||
| 	  && make install \ | ||||
| 	  && cd /usr/src/paperless/src/ \ | ||||
| 	  && rm -rf /usr/src/qpdf; \ | ||||
| 	else \ | ||||
| 	  echo "Skipping qpdf build because pikepdf binary wheels are available."; \ | ||||
| 	fi \ | ||||
|     && python3 -m pip install --upgrade pip wheel \ | ||||
| 	&& python3 -m pip install --default-timeout=1000 --upgrade --no-cache-dir supervisor \ | ||||
|   	&& python3 -m pip install --default-timeout=1000 --no-cache-dir -r ../requirements.txt \ | ||||
| 	&& apt-get -y purge build-essential git zlib1g-dev libjpeg62-turbo-dev \ | ||||
| 	&& apt-get -y autoremove --purge \ | ||||
| 	&& rm -rf /var/lib/apt/lists/* | ||||
|     build-essential \ | ||||
|     && python3 -m pip install --upgrade --no-cache-dir pip wheel \ | ||||
|   && python3 -m pip install --default-timeout=1000 --upgrade --no-cache-dir supervisor \ | ||||
|   && python3 -m pip install --default-timeout=1000 --no-cache-dir -r ../requirements.txt \ | ||||
|   && apt-get -y purge build-essential \ | ||||
|   && apt-get -y autoremove --purge \ | ||||
|   && rm -rf /var/lib/apt/lists/* | ||||
|  | ||||
| # setup docker-specific things | ||||
| COPY docker/ ./docker/ | ||||
|  | ||||
| RUN cd docker \ | ||||
|   	&& cp imagemagick-policy.xml /etc/ImageMagick-6/policy.xml \ | ||||
| 	&& mkdir /var/log/supervisord /var/run/supervisord \ | ||||
| 	&& cp supervisord.conf /etc/supervisord.conf \ | ||||
| 	&& cp docker-entrypoint.sh /sbin/docker-entrypoint.sh \ | ||||
| 	&& cp docker-prepare.sh /sbin/docker-prepare.sh \ | ||||
| 	&& chmod 755 /sbin/docker-entrypoint.sh \ | ||||
| 	&& chmod +x install_management_commands.sh \ | ||||
| 	&& ./install_management_commands.sh \ | ||||
| 	&& cd .. \ | ||||
| 	&& rm docker -rf | ||||
|     && cp imagemagick-policy.xml /etc/ImageMagick-6/policy.xml \ | ||||
|   && mkdir /var/log/supervisord /var/run/supervisord \ | ||||
|   && cp supervisord.conf /etc/supervisord.conf \ | ||||
|   && cp docker-entrypoint.sh /sbin/docker-entrypoint.sh \ | ||||
|   && chmod 755 /sbin/docker-entrypoint.sh \ | ||||
|   && cp docker-prepare.sh /sbin/docker-prepare.sh \ | ||||
|   && chmod 755 /sbin/docker-prepare.sh \ | ||||
|   && chmod +x install_management_commands.sh \ | ||||
|   && ./install_management_commands.sh \ | ||||
|   && cd .. \ | ||||
|   && rm -rf docker/ | ||||
|  | ||||
| COPY gunicorn.conf.py ../ | ||||
|  | ||||
| @@ -117,18 +53,18 @@ COPY --from=compile-frontend /src/src/ ./ | ||||
|  | ||||
| # add users, setup scripts | ||||
| RUN addgroup --gid 1000 paperless \ | ||||
| 	&& useradd --uid 1000 --gid paperless --home-dir /usr/src/paperless paperless \ | ||||
| 	&& chown -R paperless:paperless ../ \ | ||||
| 	&& gosu paperless python3 manage.py collectstatic --clear --no-input \ | ||||
| 	&& gosu paperless python3 manage.py compilemessages | ||||
|   && useradd --uid 1000 --gid paperless --home-dir /usr/src/paperless paperless \ | ||||
|   && chown -R paperless:paperless ../ \ | ||||
|   && gosu paperless python3 manage.py collectstatic --clear --no-input \ | ||||
|   && gosu paperless python3 manage.py compilemessages | ||||
|  | ||||
| VOLUME ["/usr/src/paperless/data", \ | ||||
|         "/usr/src/paperless/media", \ | ||||
|         "/usr/src/paperless/consume", \ | ||||
|         "/usr/src/paperless/export"] | ||||
|  | ||||
| VOLUME ["/usr/src/paperless/data", "/usr/src/paperless/media", "/usr/src/paperless/consume", "/usr/src/paperless/export"] | ||||
| ENTRYPOINT ["/sbin/docker-entrypoint.sh"] | ||||
| EXPOSE 8000 | ||||
| CMD ["/usr/local/bin/supervisord", "-c", "/etc/supervisord.conf"] | ||||
|  | ||||
| LABEL org.opencontainers.image.authors="paperless-ngx team <hello@paperless-ngx.com>" | ||||
| LABEL org.opencontainers.image.documentation="https://paperless-ngx.readthedocs.io/en/latest/" | ||||
| LABEL org.opencontainers.image.source="https://github.com/paperless-ngx/paperless-ngx" | ||||
| LABEL org.opencontainers.image.url="https://github.com/paperless-ngx/paperless-ngx" | ||||
| LABEL org.opencontainers.image.licenses="GPL-3.0-only" | ||||
| EXPOSE 8000 | ||||
|  | ||||
| CMD ["/usr/local/bin/supervisord", "-c", "/etc/supervisord.conf"] | ||||
|   | ||||
							
								
								
									
										35
									
								
								Pipfile
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								Pipfile
									
									
									
									
									
								
							| @@ -9,35 +9,36 @@ verify_ssl = true | ||||
| name = "piwheels" | ||||
|  | ||||
| [packages] | ||||
| dateparser = "~=1.1.0" | ||||
| django = "~=3.2" | ||||
| dateparser = "~=1.1" | ||||
| django = "~=4.0" | ||||
| django-cors-headers = "*" | ||||
| django-extensions = "*" | ||||
| django-filter = "~=21.1" | ||||
| django-q = "~=1.3.4" | ||||
| djangorestframework = "~=3.13.1" | ||||
| django-q = "~=1.3" | ||||
| djangorestframework = "~=3.13" | ||||
| filelock = "*" | ||||
| fuzzywuzzy = {extras = ["speedup"], version = "*"} | ||||
| gunicorn = "*" | ||||
| imap-tools = "*" | ||||
| langdetect = "*" | ||||
| numpy = "~=1.22.0" | ||||
| pathvalidate = "*" | ||||
| pillow = "~=9.0" | ||||
| pikepdf = "~=5.0" | ||||
| pillow = "~=9.1" | ||||
| # Any version update to pikepdf requires a base image update | ||||
| pikepdf = "~=5.1" | ||||
| python-gnupg = "*" | ||||
| python-dotenv = "*" | ||||
| python-dateutil = "*" | ||||
| python-magic = "*" | ||||
| psycopg2-binary = "*" | ||||
| # Any version update to psycopg2 requires a base image update | ||||
| psycopg2 = "*" | ||||
| redis = "*" | ||||
| # Pinned because aarch64 wheels and updates cause warnings when loading the classifier model. | ||||
| scikit-learn="==0.24.0" | ||||
| scikit-learn="==1.0.2" | ||||
| whitenoise = "~=6.0.0" | ||||
| watchdog = "~=2.1.0" | ||||
| whoosh="~=2.7.4" | ||||
| inotifyrecursive = "~=0.3.4" | ||||
| ocrmypdf = "~=13.4.0" | ||||
| inotifyrecursive = "~=0.3" | ||||
| ocrmypdf = "~=13.4" | ||||
| tqdm = "*" | ||||
| tika = "*" | ||||
| # TODO: This will sadly also install daphne+dependencies, | ||||
| @@ -46,11 +47,12 @@ channels = "~=3.0" | ||||
| channels-redis = "*" | ||||
| uvicorn = {extras = ["standard"], version = "*"} | ||||
| concurrent-log-handler = "*" | ||||
| # uvloop 0.15+ incompatible with python 3.6 | ||||
| uvloop = "~=0.16" | ||||
| cryptography = "~=36.0.1" | ||||
| "pdfminer.six" = "*" | ||||
| "backports.zoneinfo" = "*" | ||||
| "backports.zoneinfo" = {version = "*", markers = "python_version < '3.9'"} | ||||
| "importlib-resources" = {version = "*", markers = "python_version < '3.9'"} | ||||
| zipp = {version = "*", markers = "python_version < '3.9'"} | ||||
| pyzbar = "*" | ||||
| pdf2image = "*" | ||||
|  | ||||
| [dev-packages] | ||||
| coveralls = "*" | ||||
| @@ -62,7 +64,8 @@ pytest-django = "*" | ||||
| pytest-env = "*" | ||||
| pytest-sugar = "*" | ||||
| pytest-xdist = "*" | ||||
| sphinx = "~=3.4.2" | ||||
| sphinx = "~=4.5.0" | ||||
| sphinx_rtd_theme = "*" | ||||
| tox = "*" | ||||
| black = "*" | ||||
| pre-commit = "*" | ||||
|   | ||||
							
								
								
									
										1153
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1153
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										73
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										73
									
								
								README.md
									
									
									
									
									
								
							| @@ -10,23 +10,23 @@ | ||||
| </p> | ||||
|  | ||||
| <!-- omit in toc --> | ||||
|  | ||||
| # Paperless-ngx | ||||
|  | ||||
| Paperless-ngx is a document management system that transforms your physical documents into a searchable online archive so you can keep, well, *less paper*. | ||||
| Paperless-ngx is a document management system that transforms your physical documents into a searchable online archive so you can keep, well, _less paper_. | ||||
|  | ||||
| Paperless-ngx forked from [paperless-ng](https://github.com/jonaswinkler/paperless-ng) to continue the great work and distribute responsibility of supporting and advancing the project among a team of people. [Consider joining us!](#community-support) Discussion of this transition can be found in issues | ||||
| [#1599](https://github.com/jonaswinkler/paperless-ng/issues/1599) and [#1632](https://github.com/jonaswinkler/paperless-ng/issues/1632). | ||||
|  | ||||
| A demo is available at [demo.paperless-ngx.com](https://demo.paperless-ngx.com) using login `demo` / `demo`. *Note: demo content is reset frequently and confidential information should not be uploaded.* | ||||
|  | ||||
| A demo is available at [demo.paperless-ngx.com](https://demo.paperless-ngx.com) using login `demo` / `demo`. _Note: demo content is reset frequently and confidential information should not be uploaded._ | ||||
|  | ||||
| - [Features](#features) | ||||
| - [Getting started](#getting-started) | ||||
| - [Contributing](#contributing) | ||||
| 	- [Community Support](#community-support) | ||||
| 	- [Translation](#translation) | ||||
| 	- [Feature Requests](#feature-requests) | ||||
| 	- [Bugs](#bugs) | ||||
|   - [Community Support](#community-support) | ||||
|   - [Translation](#translation) | ||||
|   - [Feature Requests](#feature-requests) | ||||
|   - [Bugs](#bugs) | ||||
| - [Affiliated Projects](#affiliated-projects) | ||||
| - [Important Note](#important-note) | ||||
|  | ||||
| @@ -35,28 +35,28 @@ A demo is available at [demo.paperless-ngx.com](https://demo.paperless-ngx.com) | ||||
|  | ||||
|  | ||||
|  | ||||
| * Organize and index your scanned documents with tags, correspondents, types, and more. | ||||
| * Performs OCR on your documents, adds selectable text to image only documents and adds tags, correspondents and document types to your documents. | ||||
| * Supports PDF documents, images, plain text files, and Office documents (Word, Excel, Powerpoint, and LibreOffice equivalents). | ||||
| 	* Office document support is optional and provided by Apache Tika (see [configuration](https://paperless-ngx.readthedocs.io/en/latest/configuration.html#tika-settings)) | ||||
| * Paperless stores your documents plain on disk. Filenames and folders are managed by paperless and their format can be configured freely. | ||||
| * Single page application front end. | ||||
| 	* Includes a dashboard that shows basic statistics and has document upload. | ||||
| 	* Filtering by tags, correspondents, types, and more. | ||||
| 	* Customizable views can be saved and displayed on the dashboard. | ||||
| * Full text search helps you find what you need. | ||||
| 	* Auto completion suggests relevant words from your documents. | ||||
| 	* Results are sorted by relevance to your search query. | ||||
| 	* Highlighting shows you which parts of the document matched the query. | ||||
| 	* Searching for similar documents ("More like this") | ||||
| * Email processing: Paperless adds documents from your email accounts. | ||||
| 	* Configure multiple accounts and filters for each account. | ||||
| 	* When adding documents from mail, paperless can move these mail to a new folder, mark them as read, flag them as important or delete them. | ||||
| * Machine learning powered document matching. | ||||
| 	* Paperless-ngx learns from your documents and will be able to automatically assign tags, correspondents and types to documents once you've stored a few documents in paperless. | ||||
| * Optimized for multi core systems: Paperless-ngx consumes multiple documents in parallel. | ||||
| * The integrated sanity checker makes sure that your document archive is in good health. | ||||
| * [More screenshots are available in the documentation](https://paperless-ngx.readthedocs.io/en/latest/screenshots.html). | ||||
| - Organize and index your scanned documents with tags, correspondents, types, and more. | ||||
| - Performs OCR on your documents, adds selectable text to image only documents and adds tags, correspondents and document types to your documents. | ||||
| - Supports PDF documents, images, plain text files, and Office documents (Word, Excel, Powerpoint, and LibreOffice equivalents). | ||||
|   - Office document support is optional and provided by Apache Tika (see [configuration](https://paperless-ngx.readthedocs.io/en/latest/configuration.html#tika-settings)) | ||||
| - Paperless stores your documents plain on disk. Filenames and folders are managed by paperless and their format can be configured freely. | ||||
| - Single page application front end. | ||||
|   - Includes a dashboard that shows basic statistics and has document upload. | ||||
|   - Filtering by tags, correspondents, types, and more. | ||||
|   - Customizable views can be saved and displayed on the dashboard. | ||||
| - Full text search helps you find what you need. | ||||
|   - Auto completion suggests relevant words from your documents. | ||||
|   - Results are sorted by relevance to your search query. | ||||
|   - Highlighting shows you which parts of the document matched the query. | ||||
|   - Searching for similar documents ("More like this") | ||||
| - Email processing: Paperless adds documents from your email accounts. | ||||
|   - Configure multiple accounts and filters for each account. | ||||
|   - When adding documents from mail, paperless can move these mail to a new folder, mark them as read, flag them as important or delete them. | ||||
| - Machine learning powered document matching. | ||||
|   - Paperless-ngx learns from your documents and will be able to automatically assign tags, correspondents and types to documents once you've stored a few documents in paperless. | ||||
| - Optimized for multi core systems: Paperless-ngx consumes multiple documents in parallel. | ||||
| - The integrated sanity checker makes sure that your document archive is in good health. | ||||
| - [More screenshots are available in the documentation](https://paperless-ngx.readthedocs.io/en/latest/screenshots.html). | ||||
|  | ||||
| # Getting started | ||||
|  | ||||
| @@ -65,7 +65,7 @@ The easiest way to deploy paperless is docker-compose. The files in the [`/docke | ||||
| If you'd like to jump right in, you can configure a docker-compose environment with our install script: | ||||
|  | ||||
| ```bash | ||||
| bash -c "$(curl -L https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/master/install-paperless-ngx.sh)" | ||||
| bash -c "$(curl -L https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/install-paperless-ngx.sh)" | ||||
| ``` | ||||
|  | ||||
| Alternatively, you can install the dependencies and setup apache and a database server yourself. The [documentation](https://paperless-ngx.readthedocs.io/en/latest/setup.html#installation) has a step by step guide on how to do it. | ||||
| @@ -73,6 +73,7 @@ Alternatively, you can install the dependencies and setup apache and a database | ||||
| Migrating from Paperless-ng is easy, just drop in the new docker image! See the [documentation on migrating](https://paperless-ngx.readthedocs.io/en/latest/setup.html#migrating-from-paperless-ng) for more details. | ||||
|  | ||||
| <!-- omit in toc --> | ||||
|  | ||||
| ### Documentation | ||||
|  | ||||
| The documentation for Paperless-ngx is available on [ReadTheDocs](https://paperless-ngx.readthedocs.io/). | ||||
| @@ -101,18 +102,18 @@ For bugs please [open an issue](https://github.com/paperless-ngx/paperless-ngx/i | ||||
|  | ||||
| Paperless has been around a while now, and people are starting to build stuff on top of it. If you're one of those people, we can add your project to this list: | ||||
|  | ||||
| * [Paperless App](https://github.com/bauerj/paperless_app): An Android/iOS app for Paperless-ngx. Also works with the original Paperless and Paperless-ng. | ||||
| * [Paperless Share](https://github.com/qcasey/paperless_share). Share any files from your Android application with paperless. Very simple, but works with all of the mobile scanning apps out there that allow you to share scanned documents. | ||||
| * [Scan to Paperless](https://github.com/sbrunner/scan-to-paperless): Scan and prepare (crop, deskew, OCR, ...) your documents for Paperless. | ||||
| - [Paperless App](https://github.com/bauerj/paperless_app): An Android/iOS app for Paperless-ngx. Also works with the original Paperless and Paperless-ngx. | ||||
| - [Paperless Share](https://github.com/qcasey/paperless_share). Share any files from your Android application with paperless. Very simple, but works with all of the mobile scanning apps out there that allow you to share scanned documents. | ||||
| - [Scan to Paperless](https://github.com/sbrunner/scan-to-paperless): Scan and prepare (crop, deskew, OCR, ...) your documents for Paperless. | ||||
|  | ||||
| These projects also exist, but their status and compatibility with paperless-ngx is unknown. | ||||
|  | ||||
| * [paperless-cli](https://github.com/stgarf/paperless-cli): A golang command line binary to interact with a Paperless instance. | ||||
| - [paperless-cli](https://github.com/stgarf/paperless-cli): A golang command line binary to interact with a Paperless instance. | ||||
|  | ||||
| This project also exists, but needs updates to be compatible with paperless-ngx. | ||||
|  | ||||
| * [Paperless Desktop](https://github.com/thomasbrueggemann/paperless-desktop): A desktop UI for your Paperless installation. Runs on Mac, Linux, and Windows. | ||||
| 	Known issues on Mac: (Could not load reminders and documents) | ||||
| - [Paperless Desktop](https://github.com/thomasbrueggemann/paperless-desktop): A desktop UI for your Paperless installation. Runs on Mac, Linux, and Windows. | ||||
|   Known issues on Mac: (Could not load reminders and documents) | ||||
|  | ||||
| # Important Note | ||||
|  | ||||
|   | ||||
| @@ -22,6 +22,10 @@ | ||||
| # Docker setup does not use the configuration file. | ||||
| # A few commonly adjusted settings are provided below. | ||||
|  | ||||
| # This is required if you will be exposing Paperless-ngx on a public domain | ||||
| # (if doing so please consider security measures such as reverse proxy) | ||||
| #PAPERLESS_URL=https://paperless.example.com | ||||
|  | ||||
| # Adjust this key if you plan to make paperless available publicly. It should | ||||
| # be a very long sequence of random characters. You don't need to remember it. | ||||
| #PAPERLESS_SECRET_KEY=change-me | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| # docker-compose file for running paperless from the Docker Hub. | ||||
| # docker-compose file for running paperless from the docker container registry. | ||||
| # This file contains everything paperless needs to run. | ||||
| # Paperless supports amd64, arm and arm64 hardware. | ||||
| # Paperless supports amd64, arm and arm64 hardware. The apache/tika image | ||||
| # does not support arm or arm64, however. | ||||
| # | ||||
| # All compose files of paperless configure paperless in the following way: | ||||
| # | ||||
| @@ -79,8 +80,9 @@ services: | ||||
|   gotenberg: | ||||
|     image: gotenberg/gotenberg:7 | ||||
|     restart: unless-stopped | ||||
|     environment: | ||||
|       CHROMIUM_DISABLE_ROUTES: 1 | ||||
|     command: | ||||
|       - "gotenberg" | ||||
|       - "--chromium-disable-routes=true" | ||||
|  | ||||
|   tika: | ||||
|     image: apache/tika | ||||
|   | ||||
							
								
								
									
										85
									
								
								docker/compose/docker-compose.sqlite-tika.arm.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								docker/compose/docker-compose.sqlite-tika.arm.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | ||||
| # docker-compose file for running paperless from the docker container registry. | ||||
| # This file contains everything paperless needs to run. | ||||
| # Paperless supports amd64, arm and arm64 hardware. | ||||
| # | ||||
| # All compose files of paperless configure paperless in the following way: | ||||
| # | ||||
| # - Paperless is (re)started on system boot, if it was running before shutdown. | ||||
| # - Docker volumes for storing data are managed by Docker. | ||||
| # - Folders for importing and exporting files are created in the same directory | ||||
| #   as this file and mounted to the correct folders inside the container. | ||||
| # - Paperless listens on port 8000. | ||||
| # | ||||
| # SQLite is used as the database. The SQLite file is stored in the data volume. | ||||
| # | ||||
| # iwishiwasaneagle/apache-tika-arm docker image is used to enable arm64 arch | ||||
| # which apache/tika does not currently support. | ||||
| # | ||||
| # In addition to that, this docker-compose file adds the following optional | ||||
| # configurations: | ||||
| # | ||||
| # - Apache Tika and Gotenberg servers are started with paperless and paperless | ||||
| #   is configured to use these services. These provide support for consuming | ||||
| #   Office documents (Word, Excel, Power Point and their LibreOffice counter- | ||||
| #   parts. | ||||
| # | ||||
| # To install and update paperless with this file, do the following: | ||||
| # | ||||
| # - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env' | ||||
| #   and '.env' into a folder. | ||||
| # - Run 'docker-compose pull'. | ||||
| # - Run 'docker-compose run --rm webserver createsuperuser' to create a user. | ||||
| # - Run 'docker-compose up -d'. | ||||
| # | ||||
| # For more extensive installation and update instructions, refer to the | ||||
| # documentation. | ||||
|  | ||||
| version: "3.4" | ||||
| services: | ||||
|   broker: | ||||
|     image: redis:6.0 | ||||
|     restart: unless-stopped | ||||
|     volumes: | ||||
|       - redisdata:/data | ||||
|  | ||||
|   webserver: | ||||
|     image: ghcr.io/paperless-ngx/paperless-ngx:latest | ||||
|     restart: unless-stopped | ||||
|     depends_on: | ||||
|       - broker | ||||
|       - gotenberg | ||||
|       - tika | ||||
|     ports: | ||||
|       - 8000:8000 | ||||
|     healthcheck: | ||||
|       test: ["CMD", "curl", "-f", "http://localhost:8000"] | ||||
|       interval: 30s | ||||
|       timeout: 10s | ||||
|       retries: 5 | ||||
|     volumes: | ||||
|       - data:/usr/src/paperless/data | ||||
|       - media:/usr/src/paperless/media | ||||
|       - ./export:/usr/src/paperless/export | ||||
|       - ./consume:/usr/src/paperless/consume | ||||
|     env_file: docker-compose.env | ||||
|     environment: | ||||
|       PAPERLESS_REDIS: redis://broker:6379 | ||||
|       PAPERLESS_TIKA_ENABLED: 1 | ||||
|       PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000 | ||||
|       PAPERLESS_TIKA_ENDPOINT: http://tika:9998 | ||||
|  | ||||
|   gotenberg: | ||||
|     image: gotenberg/gotenberg:7 | ||||
|     restart: unless-stopped | ||||
|     command: | ||||
|       - "gotenberg" | ||||
|       - "--chromium-disable-routes=true" | ||||
|  | ||||
|   tika: | ||||
|     image: iwishiwasaneagle/apache-tika-arm@sha256:a78c25ffe57ecb1a194b2859d42a61af46e9e845191512b8f1a4bf90578ffdfd | ||||
|     restart: unless-stopped | ||||
|  | ||||
| volumes: | ||||
|   data: | ||||
|   media: | ||||
|   redisdata: | ||||
| @@ -1,6 +1,7 @@ | ||||
| # docker-compose file for running paperless from the Docker Hub. | ||||
| # docker-compose file for running paperless from the docker container registry. | ||||
| # This file contains everything paperless needs to run. | ||||
| # Paperless supports amd64, arm and arm64 hardware. | ||||
| # Paperless supports amd64, arm and arm64 hardware. The apache/tika image | ||||
| # does not support arm or arm64, however. | ||||
| # | ||||
| # All compose files of paperless configure paperless in the following way: | ||||
| # | ||||
| @@ -68,8 +69,9 @@ services: | ||||
|   gotenberg: | ||||
|     image: gotenberg/gotenberg:7 | ||||
|     restart: unless-stopped | ||||
|     environment: | ||||
|       CHROMIUM_DISABLE_ROUTES: 1 | ||||
|     command: | ||||
|       - "gotenberg" | ||||
|       - "--chromium-disable-routes=true" | ||||
|  | ||||
|   tika: | ||||
|     image: apache/tika | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| #!/bin/bash | ||||
| #!/usr/bin/env bash | ||||
|  | ||||
| set -e | ||||
|  | ||||
| @@ -10,7 +10,7 @@ map_uidgid() { | ||||
| 	USERMAP_NEW_GID=${USERMAP_GID:-${USERMAP_ORIG_GID:-$USERMAP_NEW_UID}} | ||||
| 	if [[ ${USERMAP_NEW_UID} != "${USERMAP_ORIG_UID}" || ${USERMAP_NEW_GID} != "${USERMAP_ORIG_GID}" ]]; then | ||||
| 		echo "Mapping UID and GID for paperless:paperless to $USERMAP_NEW_UID:$USERMAP_NEW_GID" | ||||
| 		usermod -u "${USERMAP_NEW_UID}" paperless | ||||
| 		usermod -o -u "${USERMAP_NEW_UID}" paperless | ||||
| 		groupmod -o -g "${USERMAP_NEW_GID}" paperless | ||||
| 	fi | ||||
| } | ||||
| @@ -56,12 +56,12 @@ install_languages() { | ||||
| 		#    continue | ||||
| 		#fi | ||||
|  | ||||
| 		if dpkg -s $pkg &>/dev/null; then | ||||
| 		if dpkg -s "$pkg" &>/dev/null; then | ||||
| 			echo "Package $pkg already installed!" | ||||
| 			continue | ||||
| 		fi | ||||
|  | ||||
| 		if ! apt-cache show $pkg &>/dev/null; then | ||||
| 		if ! apt-cache show "$pkg" &>/dev/null; then | ||||
| 			echo "Package $pkg not found! :(" | ||||
| 			continue | ||||
| 		fi | ||||
| @@ -77,7 +77,7 @@ install_languages() { | ||||
| echo "Paperless-ngx docker container starting..." | ||||
|  | ||||
| # Install additional languages if specified | ||||
| if [[ ! -z "$PAPERLESS_OCR_LANGUAGES" ]]; then | ||||
| if [[ -n "$PAPERLESS_OCR_LANGUAGES" ]]; then | ||||
| 	install_languages "$PAPERLESS_OCR_LANGUAGES" | ||||
| fi | ||||
|  | ||||
|   | ||||
| @@ -6,14 +6,11 @@ wait_for_postgres() { | ||||
|  | ||||
| 	echo "Waiting for PostgreSQL to start..." | ||||
|  | ||||
| 	host="${PAPERLESS_DBHOST}" | ||||
| 	port="${PAPERLESS_DBPORT}" | ||||
| 	host="${PAPERLESS_DBHOST:=localhost}" | ||||
| 	port="${PAPERLESS_DBPORT:=5342}" | ||||
|  | ||||
| 	if [[ -z $port ]]; then | ||||
| 		port="5432" | ||||
| 	fi | ||||
|  | ||||
| 	while ! </dev/tcp/$host/$port; do | ||||
| 	while [ ! "$(pg_isready -h $host -p $port)" ]; do | ||||
|  | ||||
| 		if [ $attempt_num -eq $max_attempts ]; then | ||||
| 			echo "Unable to connect to database." | ||||
| @@ -23,7 +20,7 @@ wait_for_postgres() { | ||||
|  | ||||
| 		fi | ||||
|  | ||||
| 		attempt_num=$(expr "$attempt_num" + 1) | ||||
| 		attempt_num=$(("$attempt_num" + 1)) | ||||
| 		sleep 5 | ||||
| 	done | ||||
| } | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| #!/usr/bin/env bash | ||||
|  | ||||
| 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; | ||||
| do | ||||
| 	echo "installing $command..." | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| #!/bin/bash | ||||
| #!/usr/bin/env bash | ||||
|  | ||||
| set -e | ||||
|  | ||||
| @@ -6,10 +6,10 @@ cd /usr/src/paperless/src/ | ||||
|  | ||||
| if [[ $(id -u) == 0 ]] ; | ||||
| then | ||||
|   gosu paperless python3 manage.py management_command "$@" | ||||
| 	gosu paperless python3 manage.py management_command "$@" | ||||
| elif [[ $(id -un) == "paperless" ]] ; | ||||
| then | ||||
|   python3 manage.py management_command "$@" | ||||
| 	python3 manage.py management_command "$@" | ||||
| else | ||||
|   echo "Unknown user." | ||||
| 	echo "Unknown user." | ||||
| fi | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| FROM python:3.5.1 | ||||
| MAINTAINER Pit Kleyersburg <pitkley@googlemail.com> | ||||
|  | ||||
| # Install Sphinx and Pygments | ||||
| RUN pip install Sphinx Pygments | ||||
|   | ||||
| @@ -379,7 +379,7 @@ the naming scheme. | ||||
|  | ||||
| The command takes no arguments and processes all your documents at once. | ||||
|  | ||||
| Learn how to use :ref:`Management Utilities<Management utilities>`. | ||||
| Learn how to use :ref:`Management Utilities<utilities-management-commands>`. | ||||
|  | ||||
|  | ||||
| .. _utilities-sanity-checker: | ||||
|   | ||||
| @@ -179,13 +179,14 @@ Assumed you have ``/home/foo/paperless-ngx/scripts/post-consumption-example.sh`` | ||||
| You can pass that script into the consumer container via a host mount in your ``docker-compose.yml``. | ||||
|  | ||||
| .. code:: bash | ||||
|    ... | ||||
|    consumer: | ||||
|            ... | ||||
|            volumes: | ||||
| 					     ... | ||||
|                - /home/paperless-ngx/scripts:/path/in/container/scripts/ | ||||
|    ... | ||||
|  | ||||
|   ... | ||||
|   consumer: | ||||
|     ... | ||||
|     volumes: | ||||
|       ... | ||||
|       - /home/paperless-ngx/scripts:/path/in/container/scripts/ | ||||
|   ... | ||||
|  | ||||
| Example (docker-compose.yml): ``- /home/foo/paperless-ngx/scripts:/usr/src/paperless/scripts`` | ||||
|  | ||||
|   | ||||
| @@ -5,6 +5,87 @@ | ||||
| Changelog | ||||
| ********* | ||||
|  | ||||
| paperless-ngx 1.7.0 | ||||
| ################### | ||||
|  | ||||
| Breaking Changes | ||||
|  | ||||
| * ``PAPERLESS_URL`` is now required when using a reverse proxy. See `#674`_. | ||||
|  | ||||
| Features | ||||
|  | ||||
| * Allow setting more than one tag in mail rules `@jonasc`_ (#270) | ||||
| * global drag'n'drop `@shamoon`_ (#283). | ||||
| * Fix: download buttons should disable while waiting `@shamoon`_ (#630). | ||||
| * Update checker `@shamoon`_ (#591). | ||||
| * Show prompt on password-protected pdfs `@shamoon`_ (#564). | ||||
| * Filtering query params aka browser navigation for filtering `@shamoon`_ (#540). | ||||
| * Clickable tags in dashboard widgets `@shamoon`_ (#515). | ||||
| * Add bottom pagination `@shamoon`_ (#372). | ||||
| * Feature barcode splitter `@gador`_ (#532). | ||||
| * App loading screen `@shamoon`_ (#298). | ||||
| * Use progress bar for delayed buttons `@shamoon`_ (#415). | ||||
| * Add minimum length for documents text filter `@shamoon`_ (#401). | ||||
| * Added nav buttons in the document detail view `@GruberViktor`_ (#273). | ||||
| * Improve date keyboard input `@shamoon`_ (#253). | ||||
| * Color theming `@shamoon`_ (#243). | ||||
| * Parse dates when entered without separators `@GruberViktor`_ (#250). | ||||
|  | ||||
| Bug Fixes | ||||
|  | ||||
| * add "localhost" to ALLOWED_HOSTS `@gador`_ (#700). | ||||
| * Fix: scanners table `@qcasey`_ (#690). | ||||
| * Adds wait for file before consuming `@stumpylog`_ (#483). | ||||
| * Fix: frontend document editing erases time data `@shamoon`_ (#654). | ||||
| * Increase length of SavedViewFilterRule `@stumpylog`_ (#612). | ||||
| * Fixes attachment filename matching during mail fetching `@stumpylog`_ (#680). | ||||
| * Add ``PAPERLESS_URL`` env variable & CSRF var `@shamoon`_ (#674). | ||||
| * Fix: download buttons should disable while waiting `@shamoon`_ (#630). | ||||
| * Fixes downloaded filename, add more consumer ignore settings `@stumpylog`_ (#599). | ||||
| * FIX BUG: case-sensitive matching was not possible `@danielBreitlauch`_ (#594). | ||||
| * uses shutil.move instead of rename `@gador`_ (#617). | ||||
| * Fix npm deps 01.02.22 2 `@shamoon`_ (#610). | ||||
| * Fix npm dependencies 01.02.22 `@shamoon`_ (#600). | ||||
| * fix issue 416: implement PAPERLESS_OCR_MAX_IMAGE_PIXELS `@hacker-h`_ (#441). | ||||
| * fix: exclude cypress from build in Dockerfile `@FrankStrieter`_ (#526). | ||||
| * Corrections to pass pre-commit hooks `@schnuffle`_ (#454). | ||||
| * Fix 311 unable to click checkboxes in document list `@shamoon`_ (#313). | ||||
| * Fix imap tools bug `@stumpylog`_ (#393). | ||||
| * Fix filterable dropdown buttons arent translated `@shamoon`_ (#366). | ||||
| * Fix 224: "Auto-detected date is day before receipt date" `@a17t`_ (#246). | ||||
| * Fix minor sphinx errors `@shamoon`_ (#322). | ||||
| * Fix page links hidden `@shamoon`_ (#314). | ||||
| * Fix: Include excluded items in dropdown count `@shamoon`_ (#263). | ||||
|  | ||||
| Translation | ||||
|  | ||||
| * `@miku323`_ contributed to Slovenian translation. | ||||
| * `@FaintGhost`_ contributed to Chinese Simplified translation. | ||||
| * `@DarkoBG79`_ contributed to Serbian translation. | ||||
| * `Kemal Secer`_ contributed to Turkish translation. | ||||
| * `@Prominence`_ contributed to Belarusian translation. | ||||
|  | ||||
| Documentation | ||||
|  | ||||
| * Fix: scanners table `@qcasey`_ (#690). | ||||
| * Add `PAPERLESS_URL` env variable & CSRF var `@shamoon`_ (#674). | ||||
| * Fixes downloaded filename, add more consumer ignore settings `@stumpylog`_ (#599). | ||||
| * fix issue 416: implement ``PAPERLESS_OCR_MAX_IMAGE_PIXELS`` `@hacker-h`_ (#441). | ||||
| * Fix minor sphinx errors `@shamoon`_ (#322). | ||||
|  | ||||
| Maintenance | ||||
|  | ||||
| * Add ``PAPERLESS_URL`` env variable & CSRF var `@shamoon`_ (#674). | ||||
| * Chore: Implement release-drafter action for Changelogs `@qcasey`_ (#669). | ||||
| * Chore: Add CODEOWNERS `@qcasey`_ (#667). | ||||
| * Support docker-compose v2 in install `@stumpylog`_ (#611). | ||||
| * Add Belarusian localization `@shamoon`_ (#588). | ||||
| * Add Turkish localization `@shamoon`_ (#536). | ||||
| * Add Serbian localization `@shamoon`_ (#504). | ||||
| * Create PULL_REQUEST_TEMPLATE.md `@shamoon`_ (#304). | ||||
| * Add Chinese localization `@shamoon`_ (#247). | ||||
| * Add Slovenian language for frontend `@shamoon`_ (#315). | ||||
|  | ||||
| paperless-ngx 1.6.0 | ||||
| ################### | ||||
|  | ||||
| @@ -144,7 +225,7 @@ paperless-ng 1.4.0 | ||||
|  | ||||
|   * New URL pattern for accessing documents by ASN directly (http://<paperless>/asn/123) | ||||
|  | ||||
|   * Added logging when executing pre- and post-consume scripts. | ||||
|   * Added logging when executing pre* and post-consume scripts. | ||||
|  | ||||
|   * Better error logging during document consumption. | ||||
|  | ||||
| @@ -1580,6 +1661,16 @@ bulk of the work on this big change. | ||||
| .. _@azapater: https://github.com/azapater | ||||
| .. _@tim-vogel: https://github.com/tim-vogel | ||||
| .. _@jschpp: https://github.com/jschpp | ||||
| .. _@schnuffle: https://github.com/schnuffle | ||||
| .. _@GruberViktor: https://github.com/gruberviktor | ||||
| .. _@hacker-h: https://github.com/hacker-h | ||||
| .. _@danielBreitlauch: https://github.com/danielbreitlauch | ||||
| .. _@miku323: https://github.com/miku323 | ||||
| .. _@FaintGhost: https://github.com/FaintGhost | ||||
| .. _@DarkoBG79: https://github.com/DarkoBG79 | ||||
| .. _Kemal Secer: https://crowdin.com/profile/kemal.secer | ||||
| .. _@Prominence: https://github.com/Prominence | ||||
| .. _@jonasc: https://github.com/jonasc | ||||
|  | ||||
| .. _#20: https://github.com/the-paperless-project/paperless/issues/20 | ||||
| .. _#44: https://github.com/the-paperless-project/paperless/issues/44 | ||||
| @@ -1688,6 +1779,7 @@ bulk of the work on this big change. | ||||
| .. _#488: https://github.com/the-paperless-project/paperless/pull/488 | ||||
| .. _#489: https://github.com/the-paperless-project/paperless/pull/489 | ||||
| .. _#492: https://github.com/the-paperless-project/paperless/pull/492 | ||||
| .. _#674: https://github.com/paperless-ngx/paperless-ngx/pull/674 | ||||
|  | ||||
| .. _a new home on Docker Hub: https://hub.docker.com/r/danielquinn/paperless/ | ||||
| .. _optipng: http://optipng.sourceforge.net/ | ||||
|   | ||||
| @@ -130,6 +130,8 @@ PAPERLESS_LOGROTATE_MAX_BACKUPS=<num> | ||||
|  | ||||
|     Defaults to 20. | ||||
|  | ||||
| .. _hosting-and-security: | ||||
|  | ||||
| Hosting & Security | ||||
| ################## | ||||
|  | ||||
| @@ -142,7 +144,24 @@ PAPERLESS_SECRET_KEY=<key> | ||||
|  | ||||
|     Default is listed in the file ``src/paperless/settings.py``. | ||||
|  | ||||
| PAPERLESS_ALLOWED_HOSTS<comma-separated-list> | ||||
| PAPERLESS_URL=<url> | ||||
|     This setting can be used to set the three options below (ALLOWED_HOSTS, | ||||
|     CORS_ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS). If the other options are | ||||
|     set the values will be combined with this one. Do not include a trailing | ||||
|     slash. E.g. https://paperless.domain.com | ||||
|  | ||||
|     Defaults to empty string, leaving the other settings unaffected. | ||||
|  | ||||
| PAPERLESS_CSRF_TRUSTED_ORIGINS=<comma-separated-list> | ||||
|     A list of trusted origins for unsafe requests (e.g. POST). As of Django 4.0 | ||||
|     this is required to access the Django admin via the web. | ||||
|     See https://docs.djangoproject.com/en/4.0/ref/settings/#csrf-trusted-origins | ||||
|  | ||||
|     Can also be set using PAPERLESS_URL (see above). | ||||
|  | ||||
|     Defaults to empty string, which does not add any origins to the trusted list. | ||||
|  | ||||
| PAPERLESS_ALLOWED_HOSTS=<comma-separated-list> | ||||
|     If you're planning on putting Paperless on the open internet, then you | ||||
|     really should set this value to the domain name you're using.  Failing to do | ||||
|     so leaves you open to HTTP host header attacks: | ||||
| @@ -151,12 +170,19 @@ PAPERLESS_ALLOWED_HOSTS<comma-separated-list> | ||||
|     Just remember that this is a comma-separated list, so "example.com" is fine, | ||||
|     as is "example.com,www.example.com", but NOT " example.com" or "example.com," | ||||
|  | ||||
|     Can also be set using PAPERLESS_URL (see above). | ||||
|  | ||||
|     If manually set, please remember to include "localhost". Otherwise docker | ||||
|     healthcheck will fail. | ||||
|  | ||||
|     Defaults to "*", which is all hosts. | ||||
|  | ||||
| PAPERLESS_CORS_ALLOWED_HOSTS<comma-separated-list> | ||||
| PAPERLESS_CORS_ALLOWED_HOSTS=<comma-separated-list> | ||||
|     You need to add your servers to the list of allowed hosts that can do CORS | ||||
|     calls. Set this to your public domain name. | ||||
|  | ||||
|     Can also be set using PAPERLESS_URL (see above). | ||||
|  | ||||
|     Defaults to "http://localhost:8000". | ||||
|  | ||||
| PAPERLESS_FORCE_SCRIPT_NAME=<path> | ||||
| @@ -389,6 +415,15 @@ PAPERLESS_OCR_IMAGE_DPI=<num> | ||||
|     Default is none, which will automatically calculate image DPI so that | ||||
|     the produced PDF documents are A4 sized. | ||||
|  | ||||
| PAPERLESS_OCR_MAX_IMAGE_PIXELS=<num> | ||||
|     Paperless will not OCR images that have more pixels than this limit. | ||||
|     This is intended to prevent decompression bombs from overloading paperless. | ||||
|     Increasing this limit is desired if you face a DecompressionBombError despite | ||||
|     the concerning file not being malicious; this could e.g. be caused by invalidly | ||||
|     recognized metadata. | ||||
|     If you have enough resources or if you are certain that your uploaded files | ||||
|     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. | ||||
|  | ||||
| PAPERLESS_OCR_USER_ARGS=<json> | ||||
|     OCRmyPDF offers many more options. Use this parameter to specify any | ||||
| @@ -462,8 +497,9 @@ requires are as follows: | ||||
|         gotenberg: | ||||
|             image: gotenberg/gotenberg:7 | ||||
|             restart: unless-stopped | ||||
|             environment: | ||||
|                 CHROMIUM_DISABLE_ROUTES: 1 | ||||
|             command: | ||||
|                 - "gotenberg" | ||||
|                 - "--chromium-disable-routes=true" | ||||
|  | ||||
|         tika: | ||||
|             image: apache/tika | ||||
| @@ -473,6 +509,8 @@ Add the configuration variables to the environment of the webserver (alternative | ||||
| put the configuration in the ``docker-compose.env`` file) and add the additional | ||||
| services below the webserver service. Watch out for indentation. | ||||
|  | ||||
| Make sure to use the correct format `PAPERLESS_TIKA_ENABLED = 1` so python_dotenv can parse the statement correctly. | ||||
|  | ||||
| Software tweaks | ||||
| ############### | ||||
|  | ||||
| @@ -528,6 +566,10 @@ PAPERLESS_WORKER_TIMEOUT=<num> | ||||
|     large documents within the default 1800 seconds. So extending this timeout | ||||
|     may prove to be useful on weak hardware setups. | ||||
|  | ||||
| PAPERLESS_WORKER_RETRY=<num> | ||||
|     If PAPERLESS_WORKER_TIMEOUT has been configured, the retry time for a task can | ||||
|     also be configured.  By default, this value will be set to 10s more than the | ||||
|     worker timeout.  This value should never be set less than the worker timeout. | ||||
|  | ||||
| PAPERLESS_TIME_ZONE=<timezone> | ||||
|     Set the time zone here. | ||||
| @@ -576,6 +618,27 @@ PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=<bool> | ||||
|  | ||||
|     Defaults to false. | ||||
|  | ||||
| PAPERLESS_CONSUMER_ENABLE_BARCODES=<bool> | ||||
|     Enables the scanning and page separation based on detected barcodes. | ||||
|     This allows for scanning and adding multiple documents per uploaded | ||||
|     file, which are separated by one or multiple barcode pages. | ||||
|  | ||||
|     For ease of use, it is suggested to use a standardized separation page, | ||||
|     e.g. `here <https://www.alliancegroup.co.uk/patch-codes.htm>`_. | ||||
|  | ||||
|     If no barcodes are detected in the uploaded file, no page separation | ||||
|     will happen. | ||||
|  | ||||
|     Defaults to false. | ||||
|  | ||||
|  | ||||
| PAPERLESS_CONSUMER_BARCODE_STRING=PATCHT | ||||
|   Defines the string to be detected as a separator barcode. | ||||
|   If paperless is used with the PATCH-T separator pages, users | ||||
|   shouldn't change this. | ||||
|  | ||||
|   Defaults to "PATCHT" | ||||
|  | ||||
|  | ||||
| PAPERLESS_CONVERT_MEMORY_LIMIT=<num> | ||||
|     On smaller systems, or even in the case of Very Large Documents, the consumer | ||||
| @@ -659,7 +722,7 @@ PAPERLESS_CONSUMER_IGNORE_PATTERNS=<json> | ||||
|  | ||||
|     This can be adjusted by configuring a custom json array with patterns to exclude. | ||||
|  | ||||
|     Defaults to ``[".DS_STORE/*", "._*", ".stfolder/*"]``. | ||||
|     Defaults to ``[".DS_STORE/*", "._*", ".stfolder/*", ".stversions/*", ".localized/*", "desktop.ini"]``. | ||||
|  | ||||
| Binaries | ||||
| ######## | ||||
| @@ -752,3 +815,26 @@ PAPERLESS_OCR_LANGUAGES=<list> | ||||
|         PAPERLESS_OCR_LANGUAGE=tur | ||||
|  | ||||
|     Defaults to none, which does not install any additional languages. | ||||
|  | ||||
|  | ||||
| .. _configuration-update-checking: | ||||
|  | ||||
| Update Checking | ||||
| ############### | ||||
|  | ||||
| PAPERLESS_ENABLE_UPDATE_CHECK=<bool> | ||||
|     Enable (or disable) the automatic check for available updates. This feature is disabled | ||||
|     by default but if it is not explicitly set Paperless-ngx will show a message about this. | ||||
|  | ||||
|     If enabled, the feature works by pinging the the Github API for the latest release e.g. | ||||
|     https://api.github.com/repos/paperless-ngx/paperless-ngx/releases/latest | ||||
|     to determine whether a new version is available. | ||||
|  | ||||
|     Actual updating of the app must still be performed manually. | ||||
|  | ||||
|     Note that for users of thirdy-party containers e.g. linuxserver.io this notification | ||||
|     may be 'ahead' of a new release from the third-party maintainers. | ||||
|  | ||||
|     In either case, no tracking data is collected by the app in any way. | ||||
|  | ||||
|     Defaults to none, which disables the feature. | ||||
|   | ||||
| @@ -34,6 +34,8 @@ it fixed for everyone! | ||||
| Before contributing please review our `code of conduct`_ and other important | ||||
| information in the `contributing guidelines`_. | ||||
|  | ||||
| .. _code-formatting-with-pre-commit-hooks: | ||||
|  | ||||
| Code formatting with pre-commit Hooks | ||||
| ===================================== | ||||
|  | ||||
| @@ -85,6 +87,7 @@ To do the setup you need to perform the steps from the following chapters in a c | ||||
|             docker run -d -p 6379:6379 --restart unless-stopped redis:latest | ||||
|  | ||||
| 7.  Install the python dependencies by performing in the src/ directory. | ||||
|  | ||||
|     .. code:: shell-session | ||||
|  | ||||
|         pipenv install --dev | ||||
| @@ -139,8 +142,9 @@ Testing and code style: | ||||
| *   Run ``pytest`` in the src/ directory to execute all tests. This also generates a HTML coverage | ||||
|     report. When runnings test, paperless.conf is loaded as well. However: the tests rely on the default | ||||
|     configuration. This is not ideal. But for now, make sure no settings except for DEBUG are overridden when testing. | ||||
| *   Run ``black`` to format your code. | ||||
| *   Run ``pycodestyle`` to test your code for issues with the configured code style settings. | ||||
| *   Coding style is enforced by the Git pre-commit hooks.  These will ensure your code is formatted and do some | ||||
|     linting when you do a `git commit`. | ||||
| *   You can also run ``black`` manually to format your code | ||||
|  | ||||
|     .. note:: | ||||
|  | ||||
| @@ -182,6 +186,31 @@ X-Frame-Options are in place so that the front end behaves exactly as in product | ||||
| relies on you being logged into the back end. Without a valid session, The front end will simply | ||||
| not work. | ||||
|  | ||||
| Testing and code style: | ||||
|  | ||||
| *   The frontend code (.ts, .html, .scss) use ``prettier`` for code formatting via the Git | ||||
|     ``pre-commit`` hooks which run automatically on commit. See | ||||
|     :ref:`above <code-formatting-with-pre-commit-hooks>` for installation. You can also run this | ||||
|     via cli with a command such as | ||||
|  | ||||
|     .. code:: shell-session | ||||
|  | ||||
|         $ git ls-files -- '*.ts' | xargs pre-commit run prettier --files | ||||
|  | ||||
| *   Frontend testing uses jest and cypress. There is currently a need for significantly more | ||||
|     frontend tests. Unit tests and e2e tests, respectively, can be run non-interactively with: | ||||
|  | ||||
|     .. code:: shell-session | ||||
|  | ||||
|         $ ng test | ||||
|         $ npm run e2e:ci | ||||
|  | ||||
|     Cypress also includes a UI which can be run from within the ``src-ui`` directory with | ||||
|  | ||||
|     .. code:: shell-session | ||||
|  | ||||
|         $ ./node_modules/.bin/cypress open | ||||
|  | ||||
| In order to build the front end and serve it as part of django, execute | ||||
|  | ||||
| .. code:: shell-session | ||||
|   | ||||
							
								
								
									
										11
									
								
								docs/faq.rst
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								docs/faq.rst
									
									
									
									
									
								
							| @@ -5,11 +5,11 @@ Frequently asked questions | ||||
|  | ||||
| **Q:** *What's the general plan for Paperless-ngx?* | ||||
|  | ||||
| **A:** While Paperless-ngx is already considered largely "feature-complete" it is a community-driven  | ||||
| **A:** While Paperless-ngx is already considered largely "feature-complete" it is a community-driven | ||||
| project and development will be guided in this way. New features can be submitted via | ||||
| GitHub discussions and "up-voted" by the community but this is not a guarantee the feature | ||||
| will be implemented. This project will always be open to collaboration in the form of PRs, | ||||
| ideas etc.  | ||||
| ideas etc. | ||||
|  | ||||
| **Q:** *I'm using docker. Where are my documents?* | ||||
|  | ||||
| @@ -81,11 +81,10 @@ python requirements do not have precompiled packages for ARM / ARM64. Installati | ||||
| of these will require additional development libraries and compilation will take | ||||
| a long time. | ||||
|  | ||||
| **Q:** *How do I run this on unRaid?* | ||||
| **Q:** *How do I run this on Unraid?* | ||||
|  | ||||
| **A:** Head over to `<https://github.com/selfhosters/unRAID-CA-templates>`_, | ||||
| `Uli Fahrer <https://github.com/Tooa>`_ created a container template for that. | ||||
| I don't exactly know how to use that though, since I don't use unRaid. | ||||
| **A:** Paperless-ngx is available as `community app <https://unraid.net/community/apps?q=paperless-ngx>`_ | ||||
| in Unraid. `Uli Fahrer <https://github.com/Tooa>`_ created a container template for that. | ||||
|  | ||||
| **Q:** *How do I run this on my toaster?* | ||||
|  | ||||
|   | ||||
| @@ -13,43 +13,43 @@ that works right for you based on recommendations from other Paperless users. | ||||
| Physical scanners | ||||
| ================= | ||||
|  | ||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | ||||
| | Brand   | Model          | Supports                          | Recommended By | | ||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | ||||
| |         |                | FTP | NFS | SMB | SMTP | API [1]_ |                | | ||||
| +=========+================+=====+=====+=====+======+==========+================+ | ||||
| | Brother | `ADS-1700W`_   | yes |     | yes | yes  |          |`holzhannes`_   | | ||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | ||||
| | Brother | `ADS-1600W`_   | yes |     | yes | yes  |          |`holzhannes`_   | | ||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | ||||
| | Brother | `ADS-1500W`_   | yes |     | yes | yes  |          |`danielquinn`_  | | ||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | ||||
| | Brother | `ADS-1100W`_   | yes |     |     |      |          |`ytzelf`_       | | ||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | ||||
| | Brother | `ADS-2800W`_   | yes | yes |     | yes  | yes      |`philpagel`_    | | ||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | ||||
| | Brother | `MFC-J6930DW`_ | yes |     |     |      |          |`ayounggun`_    | | ||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | ||||
| | Brother | `MFC-L5850DW`_ | yes |     |     | yes  |          |`holzhannes`_   | | ||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | ||||
| | Brother | `MFC-L2750DW`_ | yes |     | yes | yes  |          |`muued`_        | | ||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | ||||
| | Brother | `MFC-J5910DW`_ | yes |     |     |      |          |`bmsleight`_    | | ||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | ||||
| | Brother | `MFC-8950DW`_  | yes |     |     | yes  | yes      |`philpagel`_    | | ||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | ||||
| | Brother | `MFC-9142CDN`_ | yes |     | yes |      |          |`REOLDEV`_      | | ||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | ||||
| | Fujitsu | `ix500`_       | yes |     | yes |      |          |`eonist`_       | | ||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | ||||
| | Epson   | `ES-580W`_     | yes |     | yes | yes  |          |`fignew`_       | | ||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | ||||
| | Epson   | `WF-7710DWF`_  | yes |     | yes |      |          |`Skylinar`_     | | ||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | ||||
| | Fujitsu | `S1300i`_      | yes |     | yes |      |          |`jonaswinkler`_ | | ||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | ||||
| | Doxie   | `Q2`_          |     |     |     |      | yes      |`Unkn0wnCat`_   | | ||||
| +---------+----------------+-----+-----+-----+------+----------+----------------+ | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Brand   | Model          | Supports                                 | Recommended By | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| |         |                | FTP | SFTP | NFS | SMB | SMTP | API [1]_ |                | | ||||
| +=========+================+=====+======+=====+=====+======+==========+================+ | ||||
| | Brother | `ADS-1700W`_   | yes |      |     | yes | yes  |          |`holzhannes`_   | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Brother | `ADS-1600W`_   | yes |      |     | yes | yes  |          |`holzhannes`_   | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Brother | `ADS-1500W`_   | yes |      |     | yes | yes  |          |`danielquinn`_  | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Brother | `ADS-1100W`_   | yes |      |     |     |      |          |`ytzelf`_       | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Brother | `ADS-2800W`_   | yes | yes  |     | yes | yes  |          |`philpagel`_    | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Brother | `MFC-J6930DW`_ | yes |      |     |     |      |          |`ayounggun`_    | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Brother | `MFC-L5850DW`_ | yes |      |     |     | yes  |          |`holzhannes`_   | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Brother | `MFC-L2750DW`_ | yes |      |     | yes | yes  |          |`muued`_        | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Brother | `MFC-J5910DW`_ | yes |      |     |     |      |          |`bmsleight`_    | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Brother | `MFC-8950DW`_  | yes |      |     | yes | yes  |          |`philpagel`_    | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Brother | `MFC-9142CDN`_ | yes |      |     | yes |      |          |`REOLDEV`_      | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Fujitsu | `ix500`_       | yes |      |     | yes |      |          |`eonist`_       | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Epson   | `ES-580W`_     | yes |      |     | yes | yes  |          |`fignew`_       | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Epson   | `WF-7710DWF`_  | yes |      |     | yes |      |          |`Skylinar`_     | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Fujitsu | `S1300i`_      | yes |      |     | yes |      |          |`jonaswinkler`_ | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
| | Doxie   | `Q2`_          |     |      |     |     |      | yes      |`Unkn0wnCat`_   | | ||||
| +---------+----------------+-----+------+-----+-----+------+----------+----------------+ | ||||
|  | ||||
| .. _MFC-L5850DW: https://www.brother-usa.com/products/mfcl5850dw | ||||
| .. _MFC-L2750DW: https://www.brother.de/drucker/laserdrucker/mfc-l2750dw | ||||
| @@ -131,4 +131,3 @@ This part assumes your Doxie is connected to WiFi and you know its IP. | ||||
| 6. Click *Submit* at the bottom of the page | ||||
|  | ||||
| Congrats, you can now scan directly from your Doxie to your Paperless-ngx instance! | ||||
|  | ||||
|   | ||||
| @@ -110,7 +110,7 @@ performs all the steps described in :ref:`setup-docker_hub` automatically. | ||||
|  | ||||
|     .. code:: shell-session | ||||
|  | ||||
|         $ bash -c "$(curl -L https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/master/install-paperless-ngx.sh)" | ||||
|         $ bash -c "$(curl -L https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/install-paperless-ngx.sh)" | ||||
|  | ||||
| .. _setup-docker_hub: | ||||
|  | ||||
| @@ -481,7 +481,7 @@ Migrating from Paperless-ng | ||||
| =========================== | ||||
|  | ||||
| Paperless-ngx is meant to be a drop-in replacement for Paperless-ng and thus upgrading should be | ||||
| trivial for most users, especially when using docker. However, as with any major change, it is  | ||||
| trivial for most users, especially when using docker. However, as with any major change, it is | ||||
| recommended to take a full backup first. Once you are ready, simply change the docker image to | ||||
| point to the new source. E.g. if using Docker Compose, edit ``docker-compose.yml`` and change: | ||||
|  | ||||
| @@ -494,12 +494,12 @@ to | ||||
| .. code:: | ||||
|  | ||||
|   image: ghcr.io/paperless-ngx/paperless-ngx:latest | ||||
|      | ||||
|  | ||||
| and then run ``docker-compose up -d`` which will pull the new image recreate the container. | ||||
| That's it! | ||||
|  | ||||
| Users who installed with the bare-metal route should also update their Git clone to point to  | ||||
| ``https://github.com/paperless-ngx/paperless-ngx``, e.g. using the command  | ||||
| Users who installed with the bare-metal route should also update their Git clone to point to | ||||
| ``https://github.com/paperless-ngx/paperless-ngx``, e.g. using the command | ||||
| ``git remote set-url origin https://github.com/paperless-ngx/paperless-ngx`` and then pull the | ||||
| lastest version. | ||||
|  | ||||
| @@ -728,6 +728,8 @@ configuring some options in paperless can help improve performance immensely: | ||||
|     times. Thumbnails will be about 20% larger. | ||||
| *   If using docker, consider setting ``PAPERLESS_WEBSERVER_WORKERS`` to | ||||
|     1. This will save some memory. | ||||
| *   Use the arm compatible docker-compose if you're wanting to use Tika on something like | ||||
| 		a raspberry pi. The official apache/tika image does not support the arm architecture. | ||||
|  | ||||
| For details, refer to :ref:`configuration`. | ||||
|  | ||||
| @@ -786,4 +788,6 @@ the following configuration is required for paperless to operate: | ||||
|         } | ||||
|     } | ||||
|  | ||||
| The ``PAPERLESS_URL`` configuration variable is also required when using a reverse proxy. Please refer to the :ref:`hosting-and-security` docs. | ||||
|  | ||||
| Also read `this <https://channels.readthedocs.io/en/stable/deploying.html#nginx-supervisor-ubuntu>`__, towards the end of the section. | ||||
|   | ||||
| @@ -119,7 +119,7 @@ You may experience these errors when using the optional TIKA integration: | ||||
| Gotenberg is a server that converts Office documents into PDF documents and has a default timeout of 30 seconds. | ||||
| When conversion takes longer, Gotenberg raises this error. | ||||
|  | ||||
| You can increase the timeout by configuring an environment variable for Gotenberg (see also `here <https://gotenberg.dev/docs/modules/api#properties>`__). | ||||
| You can increase the timeout by configuring a command flag for Gotenberg (see also `here <https://gotenberg.dev/docs/modules/api#properties>`__). | ||||
| If using docker-compose, this is achieved by the following configuration change in the ``docker-compose.yml`` file: | ||||
|  | ||||
| .. code:: yaml | ||||
| @@ -127,9 +127,10 @@ If using docker-compose, this is achieved by the following configuration change | ||||
|     gotenberg: | ||||
|         image: gotenberg/gotenberg:7 | ||||
|         restart: unless-stopped | ||||
|         environment: | ||||
|             CHROMIUM_DISABLE_ROUTES: 1 | ||||
|             API_PROCESS_TIMEOUT: 60 | ||||
|         command: | ||||
|             - "gotenberg" | ||||
|             - "--chromium-disable-routes=true" | ||||
|             - "--api-timeout=60" | ||||
|  | ||||
| Permission denied errors in the consumption directory | ||||
| ##################################################### | ||||
|   | ||||
| @@ -180,6 +180,14 @@ These are as follows: | ||||
|     automatically or manually and tell paperless to move them to yet another folder | ||||
|     after consumption. It's up to you. | ||||
|  | ||||
| .. note:: | ||||
|  | ||||
|     When defining a mail rule with a folder, you may need to try different characters to | ||||
|     define how the sub-folders are separated.  Common values include ".", "/" or "|", but | ||||
|     this varies by the mail server.  Unfortunately, this isn't a value we can determine | ||||
|     automatically.  Either check the documentation for your mail server, or check for | ||||
|     errors in the logs and try different folder separator values. | ||||
|  | ||||
| .. note:: | ||||
|  | ||||
|     Paperless will process the rules in the order defined in the admin page. | ||||
|   | ||||
| @@ -3,16 +3,16 @@ | ||||
| ask() { | ||||
| 	while true ; do | ||||
| 		if [[ -z $3 ]] ; then | ||||
| 			read -p "$1 [$2]: " result | ||||
| 			read -r -p "$1 [$2]: " result | ||||
| 		else | ||||
| 			read -p "$1 ($3) [$2]: " result | ||||
| 			read -r -p "$1 ($3) [$2]: " result | ||||
| 		fi | ||||
| 		if [[ -z $result ]]; then | ||||
| 			ask_result=$2 | ||||
| 			return | ||||
| 		fi | ||||
| 		array=$3 | ||||
| 		if [[ -z $3 || " ${array[@]} " =~ " ${result} " ]]; then | ||||
| 		if [[ -z $3 || " ${array[*]} " =~ ${result} ]]; then | ||||
| 			ask_result=$result | ||||
| 			return | ||||
| 		else | ||||
| @@ -24,7 +24,7 @@ ask() { | ||||
| ask_docker_folder() { | ||||
| 	while true ; do | ||||
|  | ||||
| 		read -p "$1 [$2]: " result | ||||
| 		read -r -p "$1 [$2]: " result | ||||
|  | ||||
| 		if [[ -z $result ]]; then | ||||
| 			ask_result=$2 | ||||
| @@ -47,25 +47,29 @@ if [[ $(id -u) == "0" ]] ; then | ||||
| 	exit 1 | ||||
| fi | ||||
|  | ||||
| if [[ -z $(which wget) ]] ; then | ||||
| if ! command -v wget &> /dev/null ; then | ||||
| 	echo "wget executable not found. Is wget installed?" | ||||
| 	exit 1 | ||||
| fi | ||||
|  | ||||
| if [[ -z $(which docker) ]] ; then | ||||
| if ! command -v docker &> /dev/null ; then | ||||
| 	echo "docker executable not found. Is docker installed?" | ||||
| 	exit 1 | ||||
| fi | ||||
|  | ||||
| if [[ -z $(which docker-compose) ]] ; then | ||||
| 	echo "docker-compose executable not found. Is docker-compose installed?" | ||||
| 	exit 1 | ||||
| DOCKER_COMPOSE_CMD="docker-compose" | ||||
| if ! command -v ${DOCKER_COMPOSE_CMD} ; then | ||||
| 	if docker compose version &> /dev/null ; then | ||||
| 		DOCKER_COMPOSE_CMD="docker compose" | ||||
| 	else | ||||
| 		echo "docker-compose executable not found. Is docker-compose installed?" | ||||
| 		exit 1 | ||||
| 	fi | ||||
| fi | ||||
|  | ||||
| # Check if user has permissions to run Docker by trying to get the status of Docker (docker status). | ||||
| # If this fails, the user probably does not have permissions for Docker. | ||||
| docker stats --no-stream 2>/dev/null 1>&2 | ||||
| if [ $? -ne 0 ] ; then | ||||
| if ! docker stats --no-stream &> /dev/null ; then | ||||
| 	echo "" | ||||
| 	echo "WARN: It look like the current user does not have Docker permissions." | ||||
| 	echo "WARN: Use 'sudo usermod -aG docker $USER' to assign Docker permissions to the user." | ||||
| @@ -88,6 +92,14 @@ echo "" | ||||
| echo "1. Application configuration" | ||||
| echo "============================" | ||||
|  | ||||
| echo "" | ||||
| echo "The URL paperless will be available at. This is required if the" | ||||
| echo "installation will be accessible via the web, otherwise can be left blank." | ||||
| echo "" | ||||
|  | ||||
| ask "URL" "" | ||||
| URL=$ask_result | ||||
|  | ||||
| echo "" | ||||
| echo "The port on which the paperless webserver will listen for incoming" | ||||
| echo "connections." | ||||
| @@ -162,7 +174,7 @@ ask "Target folder" "$(pwd)/paperless-ngx" | ||||
| TARGET_FOLDER=$ask_result | ||||
|  | ||||
| echo "" | ||||
| echo "The consume folder is where paperles will search for new documents." | ||||
| echo "The consume folder is where paperless will search for new documents." | ||||
| echo "Point this to a folder where your scanner is able to put your scanned" | ||||
| echo "documents." | ||||
| echo "" | ||||
| @@ -228,7 +240,7 @@ ask "Paperless username" "$(whoami)" | ||||
| USERNAME=$ask_result | ||||
|  | ||||
| while true; do | ||||
| 	read -sp "Paperless password: " PASSWORD | ||||
| 	read -r -sp "Paperless password: " PASSWORD | ||||
| 	echo "" | ||||
|  | ||||
| 	if [[ -z $PASSWORD ]] ; then | ||||
| @@ -236,7 +248,7 @@ while true; do | ||||
| 		continue | ||||
| 	fi | ||||
|  | ||||
| 	read -sp "Paperless password (again): " PASSWORD_REPEAT | ||||
| 	read -r -sp "Paperless password (again): " PASSWORD_REPEAT | ||||
| 	echo "" | ||||
|  | ||||
| 	if [[ ! "$PASSWORD" == "$PASSWORD_REPEAT" ]] ; then | ||||
| @@ -274,6 +286,7 @@ if [[ "$DATABASE_BACKEND" == "postgres" ]] ; then | ||||
| 	fi | ||||
| fi | ||||
| echo "" | ||||
| echo "URL: $URL" | ||||
| echo "Port: $PORT" | ||||
| echo "Database: $DATABASE_BACKEND" | ||||
| echo "Tika enabled: $TIKA_ENABLED" | ||||
| @@ -285,7 +298,7 @@ echo "Paperless username: $USERNAME" | ||||
| echo "Paperless email: $EMAIL" | ||||
|  | ||||
| echo "" | ||||
| read -p "Press any key to install." | ||||
| read -r -p "Press any key to install." | ||||
|  | ||||
| echo "" | ||||
| echo "Installing paperless..." | ||||
| @@ -301,14 +314,17 @@ if [[ $TIKA_ENABLED == "yes" ]] ; then | ||||
| 	DOCKER_COMPOSE_VERSION="$DOCKER_COMPOSE_VERSION-tika" | ||||
| fi | ||||
|  | ||||
| wget "https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/master/docker/compose/docker-compose.$DOCKER_COMPOSE_VERSION.yml" -O docker-compose.yml | ||||
| wget "https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/master/docker/compose/.env" -O .env | ||||
| wget "https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/docker-compose.$DOCKER_COMPOSE_VERSION.yml" -O docker-compose.yml | ||||
| wget "https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/.env" -O .env | ||||
|  | ||||
| SECRET_KEY=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 64 | head -n 1) | ||||
| SECRET_KEY=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | fold -w 64 | head -n 1) | ||||
|  | ||||
| DEFAULT_LANGUAGES="deu eng fra ita spa" | ||||
|  | ||||
| { | ||||
| 	if [[ ! $URL == "" ]] ; then | ||||
| 		echo "PAPERLESS_URL=$URL" | ||||
| 	fi | ||||
| 	if [[ ! $USERMAP_UID == "1000" ]] ; then | ||||
| 		echo "USERMAP_UID=$USERMAP_UID" | ||||
| 	fi | ||||
| @@ -318,7 +334,7 @@ DEFAULT_LANGUAGES="deu eng fra ita spa" | ||||
| 	echo "PAPERLESS_TIME_ZONE=$TIME_ZONE" | ||||
| 	echo "PAPERLESS_OCR_LANGUAGE=$OCR_LANGUAGE" | ||||
| 	echo "PAPERLESS_SECRET_KEY=$SECRET_KEY" | ||||
| 	if [[ ! " ${DEFAULT_LANGUAGES[@]} " =~ " ${OCR_LANGUAGE} " ]] ; then | ||||
| 	if [[ ! " ${DEFAULT_LANGUAGES[*]} " =~ ${OCR_LANGUAGE} ]] ; then | ||||
| 		echo "PAPERLESS_OCR_LANGUAGES=$OCR_LANGUAGE" | ||||
| 	fi | ||||
| } > docker-compose.env | ||||
| @@ -329,18 +345,31 @@ sed -i "s#- \./consume:/usr/src/paperless/consume#- $CONSUME_FOLDER:/usr/src/pap | ||||
|  | ||||
| if [[ -n $MEDIA_FOLDER ]] ; then | ||||
| 	sed -i "s#- media:/usr/src/paperless/media#- $MEDIA_FOLDER:/usr/src/paperless/media#g" docker-compose.yml | ||||
| 	sed -i "/^\s*media:/d" docker-compose.yml | ||||
| fi | ||||
|  | ||||
| if [[ -n $DATA_FOLDER ]] ; then | ||||
| 	sed -i "s#- data:/usr/src/paperless/data#- $DATA_FOLDER:/usr/src/paperless/data#g" docker-compose.yml | ||||
| 	sed -i "/^\s*data:/d" docker-compose.yml | ||||
| fi | ||||
|  | ||||
| if [[ -n $POSTGRES_FOLDER ]] ; then | ||||
| 	sed -i "s#- pgdata:/var/lib/postgresql/data#- $POSTGRES_FOLDER:/var/lib/postgresql/data#g" docker-compose.yml | ||||
| 	sed -i "/^\s*pgdata:/d" docker-compose.yml | ||||
| fi | ||||
|  | ||||
| docker-compose pull | ||||
| # remove trailing blank lines from end of file | ||||
| sed -i -e :a -e '/^\n*$/{$d;N;};/\n$/ba' docker-compose.yml | ||||
| # if last line in file contains "volumes:", remove that line since no more named volumes are left | ||||
| l1=$(grep -n '^volumes:' docker-compose.yml | cut -d : -f 1)  # get line number containing volume: at begin of line | ||||
| l2=$(wc -l < docker-compose.yml)  # get total number of lines | ||||
| if [ "$l1" -eq "$l2" ] ; then | ||||
| 	sed -i "/^volumes:/d" docker-compose.yml | ||||
| fi | ||||
|  | ||||
| docker-compose run --rm -e DJANGO_SUPERUSER_PASSWORD="$PASSWORD" webserver createsuperuser --noinput --username "$USERNAME" --email "$EMAIL" | ||||
|  | ||||
| docker-compose up -d | ||||
| ${DOCKER_COMPOSE_CMD} pull | ||||
|  | ||||
| ${DOCKER_COMPOSE_CMD} run --rm -e DJANGO_SUPERUSER_PASSWORD="$PASSWORD" webserver createsuperuser --noinput --username "$USERNAME" --email "$EMAIL" | ||||
|  | ||||
| ${DOCKER_COMPOSE_CMD} up -d | ||||
|   | ||||
| @@ -27,8 +27,10 @@ | ||||
| # Security and hosting | ||||
|  | ||||
| #PAPERLESS_SECRET_KEY=change-me | ||||
| #PAPERLESS_ALLOWED_HOSTS=example.com,www.example.com | ||||
| #PAPERLESS_CORS_ALLOWED_HOSTS=http://example.com,http://localhost:8000 | ||||
| #PAPERLESS_URL=https://example.com | ||||
| #PAPERLESS_CSRF_TRUSTED_ORIGINS=https://example.com # can be set using PAPERLESS_URL | ||||
| #PAPERLESS_ALLOWED_HOSTS=example.com,www.example.com # can be set using PAPERLESS_URL | ||||
| #PAPERLESS_CORS_ALLOWED_HOSTS=https://localhost:8080,https://example.com # can be set using PAPERLESS_URL | ||||
| #PAPERLESS_FORCE_SCRIPT_NAME= | ||||
| #PAPERLESS_STATIC_URL=/static/ | ||||
| #PAPERLESS_AUTO_LOGIN_USERNAME= | ||||
| @@ -58,8 +60,10 @@ | ||||
| #PAPERLESS_CONSUMER_POLLING=10 | ||||
| #PAPERLESS_CONSUMER_DELETE_DUPLICATES=false | ||||
| #PAPERLESS_CONSUMER_RECURSIVE=false | ||||
| #PAPERLESS_CONSUMER_IGNORE_PATTERNS=[".DS_STORE/*", "._*", ".stfolder/*"] | ||||
| #PAPERLESS_CONSUMER_IGNORE_PATTERNS=[".DS_STORE/*", "._*", ".stfolder/*", ".stversions/*", ".localized/*", "desktop.ini"] | ||||
| #PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=false | ||||
| #PAPERLESS_CONSUMER_ENABLE_BARCODES=false | ||||
| #PAPERLESS_CONSUMER_ENABLE_BARCODES=PATCHT | ||||
| #PAPERLESS_OPTIMIZE_THUMBNAILS=true | ||||
| #PAPERLESS_PRE_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh | ||||
| #PAPERLESS_POST_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh | ||||
| @@ -67,6 +71,7 @@ | ||||
| #PAPERLESS_FILENAME_PARSE_TRANSFORMS=[] | ||||
| #PAPERLESS_THUMBNAIL_FONT_NAME= | ||||
| #PAPERLESS_IGNORE_DATES= | ||||
| #PAPERLESS_ENABLE_UPDATE_CHECK= | ||||
|  | ||||
| # Tika settings | ||||
|  | ||||
|   | ||||
| @@ -5,49 +5,50 @@ | ||||
| #    pipenv lock --requirements | ||||
| # | ||||
|  | ||||
| -i https://pypi.python.org/simple | ||||
| --extra-index-url https://www.piwheels.org/simple | ||||
| -i https://pypi.python.org/simple/ | ||||
| --extra-index-url https://www.piwheels.org/simple/ | ||||
| aioredis==1.3.1 | ||||
| anyio==3.5.0; python_full_version >= '3.6.2' | ||||
| arrow==1.2.2; python_version >= '3.6' | ||||
| asgiref==3.5.0; python_version >= '3.7' | ||||
| 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' | ||||
| autobahn==22.2.2; python_version >= '3.7' | ||||
| autobahn==22.3.2; python_version >= '3.7' | ||||
| automat==20.2.0 | ||||
| backports.zoneinfo==0.2.1 | ||||
| backports.zoneinfo==0.2.1; python_version < '3.9' | ||||
| blessed==1.19.1; python_version >= '2.7' | ||||
| certifi==2021.10.8 | ||||
| cffi==1.15.0 | ||||
| channels-redis==3.3.1 | ||||
| channels-redis==3.4.0 | ||||
| channels==3.0.4 | ||||
| chardet==4.0.0; python_version >= '3.1' | ||||
| charset-normalizer==2.0.12; python_version >= '3' | ||||
| click==8.0.4; python_version >= '3.6' | ||||
| click==8.1.2; 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' | ||||
| concurrent-log-handler==0.9.20 | ||||
| constantly==15.1.0 | ||||
| cryptography==36.0.1 | ||||
| cryptography==36.0.2; python_version >= '3.6' | ||||
| daphne==3.0.2; python_version >= '3.6' | ||||
| dateparser==1.1.0 | ||||
| dateparser==1.1.1 | ||||
| django-cors-headers==3.11.0 | ||||
| django-extensions==3.1.5 | ||||
| django-filter==21.1 | ||||
| django-picklefield==3.0.1; python_version >= '3' | ||||
| django-q==1.3.9 | ||||
| django==3.2.12 | ||||
| django==4.0.4 | ||||
| djangorestframework==3.13.1 | ||||
| filelock==3.6.0 | ||||
| fuzzywuzzy[speedup]==0.18.0 | ||||
| gunicorn==20.1.0 | ||||
| h11==0.13.0; python_version >= '3.6' | ||||
| hiredis==2.0.0; python_version >= '3.6' | ||||
| httptools==0.3.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' | ||||
| hyperlink==21.0.0 | ||||
| idna==3.3; python_version >= '3.5' | ||||
| imap-tools==0.51.1 | ||||
| img2pdf==0.4.3 | ||||
| importlib-resources==5.4.0; python_version < '3.9' | ||||
| imap-tools==0.53.0 | ||||
| img2pdf==0.4.4 | ||||
| importlib-resources==5.6.0; python_version < '3.9' | ||||
| 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' | ||||
| inotifyrecursive==0.3.5 | ||||
| @@ -55,55 +56,58 @@ joblib==1.1.0; python_version >= '3.6' | ||||
| 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' | ||||
| msgpack==1.0.3 | ||||
| numpy==1.22.2 | ||||
| ocrmypdf==13.4.0 | ||||
| numpy==1.22.3; python_version >= '3.8' | ||||
| ocrmypdf==13.4.2 | ||||
| packaging==21.3; python_version >= '3.6' | ||||
| pathvalidate==2.5.0 | ||||
| pdfminer.six==20211012 | ||||
| pikepdf==5.0.1 | ||||
| pillow==9.0.1 | ||||
| pdf2image==1.16.0 | ||||
| pdfminer.six==20220319 | ||||
| pikepdf==5.1.1 | ||||
| pillow==9.1.0 | ||||
| pluggy==1.0.0; python_version >= '3.6' | ||||
| portalocker==2.4.0; python_version >= '3' | ||||
| psycopg2-binary==2.9.3 | ||||
| psycopg2==2.9.3 | ||||
| pyasn1-modules==0.2.8 | ||||
| pyasn1==0.4.8 | ||||
| pycparser==2.21; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' | ||||
| pyopenssl==22.0.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' | ||||
| pyparsing==3.0.7; python_version >= '3.6' | ||||
| pycparser==2.21 | ||||
| pyopenssl==22.0.0 | ||||
| pyparsing==3.0.8; python_full_version >= '3.6.8' | ||||
| python-dateutil==2.8.2 | ||||
| python-dotenv==0.19.2 | ||||
| python-dotenv==0.20.0 | ||||
| python-gnupg==0.4.8 | ||||
| python-levenshtein==0.12.2 | ||||
| python-magic==0.4.25 | ||||
| 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==2021.3 | ||||
| pytz==2022.1 | ||||
| pyyaml==6.0 | ||||
| pyzbar==0.1.9 | ||||
| redis==3.5.3 | ||||
| regex==2022.1.18 | ||||
| reportlab==3.6.7; python_version >= '3.6' and python_version < '4' | ||||
| regex==2022.3.2; python_version >= '3.6' | ||||
| reportlab==3.6.9; 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' | ||||
| scikit-learn==0.24.0 | ||||
| scikit-learn==1.0.2 | ||||
| scipy==1.8.0; python_version < '3.11' and python_version >= '3.8' | ||||
| service-identity==21.1.0 | ||||
| setuptools==60.9.3; python_version >= '3.7' | ||||
| setuptools==62.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' | ||||
| sniffio==1.2.0; python_version >= '3.5' | ||||
| sqlparse==0.4.2; python_version >= '3.5' | ||||
| threadpoolctl==3.1.0; python_version >= '3.6' | ||||
| tika==1.24 | ||||
| tqdm==4.62.3 | ||||
| twisted[tls]==22.1.0; python_full_version >= '3.6.7' | ||||
| tqdm==4.64.0 | ||||
| twisted[tls]==22.4.0; python_full_version >= '3.6.7' | ||||
| txaio==22.2.1; python_version >= '3.6' | ||||
| typing-extensions==4.1.1; python_version >= '3.6' | ||||
| tzdata==2021.5; python_version >= '3.6' | ||||
| tzlocal==4.1; python_version >= '3.6' | ||||
| urllib3==1.26.8; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4' | ||||
| uvicorn[standard]==0.17.5 | ||||
| tzdata==2022.1; 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' | ||||
| uvicorn[standard]==0.17.6 | ||||
| uvloop==0.16.0 | ||||
| watchdog==2.1.6 | ||||
| watchgod==0.7 | ||||
| watchdog==2.1.7 | ||||
| watchgod==0.8.2 | ||||
| wcwidth==0.2.5 | ||||
| websockets==10.2 | ||||
| whitenoise==6.0.0 | ||||
| whoosh==2.7.4 | ||||
| zipp==3.7.0; python_version < '3.10' | ||||
| 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' | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| #!/usr/bin/env bash | ||||
|  | ||||
| docker run -p 5432:5432 -e POSTGRES_PASSWORD=password -v paperless_pgdata:/var/lib/postgresql/data -d postgres:13 | ||||
| docker run -d -p 6379:6379 redis:latest | ||||
| docker run -p 3000:3000 -d gotenberg/gotenberg:7 | ||||
|   | ||||
							
								
								
									
										4
									
								
								src-ui/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								src-ui/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -45,3 +45,7 @@ testem.log | ||||
| # System Files | ||||
| .DS_Store | ||||
| Thumbs.db | ||||
|  | ||||
| # Cypress | ||||
| cypress/videos/**/* | ||||
| cypress/screenshots/**/* | ||||
|   | ||||
| @@ -16,6 +16,7 @@ | ||||
| 			"i18n": { | ||||
| 				"sourceLocale": "en-US", | ||||
| 				"locales": { | ||||
| 					"be-BY": "src/locale/messages.be_BY.xlf", | ||||
| 					"cs-CZ": "src/locale/messages.cs_CZ.xlf", | ||||
| 					"da-DK": "src/locale/messages.da_DK.xlf", | ||||
| 					"de-DE": "src/locale/messages.de_DE.xlf", | ||||
| @@ -30,8 +31,12 @@ | ||||
| 					"pt-PT": "src/locale/messages.pt_PT.xlf", | ||||
| 					"ro-RO": "src/locale/messages.ro_RO.xlf", | ||||
| 					"ru-RU": "src/locale/messages.ru_RU.xlf", | ||||
| 					"sv-SE": "src/locale/messages.sv_SE.xlf" | ||||
|         } | ||||
| 					"sl-SI": "src/locale/messages.sl_SI.xlf", | ||||
| 					"sr-CS": "src/locale/messages.sr_CS.xlf", | ||||
| 					"sv-SE": "src/locale/messages.sv_SE.xlf", | ||||
| 					"tr-TR": "src/locale/messages.tr_TR.xlf", | ||||
| 					"zh-CN": "src/locale/messages.zh_CN.xlf" | ||||
| 				} | ||||
| 			}, | ||||
| 			"architect": { | ||||
| 				"build": { | ||||
| @@ -121,12 +126,9 @@ | ||||
| 					} | ||||
| 				}, | ||||
| 				"test": { | ||||
| 					"builder": "@angular-devkit/build-angular:karma", | ||||
| 					"builder": "@angular-builders/jest:run", | ||||
| 					"options": { | ||||
| 						"main": "src/test.ts", | ||||
| 						"polyfills": "src/polyfills.ts", | ||||
| 						"tsConfig": "tsconfig.spec.json", | ||||
| 						"karmaConfig": "karma.conf.js", | ||||
| 						"assets": [ | ||||
| 							"src/favicon.ico", | ||||
| 							"src/apple-touch-icon.png", | ||||
| @@ -140,9 +142,21 @@ | ||||
| 					} | ||||
| 				}, | ||||
| 				"e2e": { | ||||
| 					"builder": "@angular-devkit/build-angular:protractor", | ||||
| 					"builder": "@cypress/schematic:cypress", | ||||
| 					"options": { | ||||
| 						"devServerTarget": "paperless-ui:serve", | ||||
| 						"watch": true, | ||||
| 						"headless": false | ||||
| 					}, | ||||
| 					"configurations": { | ||||
| 						"production": { | ||||
| 							"devServerTarget": "paperless-ui:serve:production" | ||||
| 						} | ||||
| 					} | ||||
| 				}, | ||||
| 				"cypress-run": { | ||||
| 					"builder": "@cypress/schematic:cypress", | ||||
| 					"options": { | ||||
| 						"protractorConfig": "e2e/protractor.conf.js", | ||||
| 						"devServerTarget": "paperless-ui:serve" | ||||
| 					}, | ||||
| 					"configurations": { | ||||
| @@ -150,6 +164,13 @@ | ||||
| 							"devServerTarget": "paperless-ui:serve:production" | ||||
| 						} | ||||
| 					} | ||||
| 				}, | ||||
| 				"cypress-open": { | ||||
| 					"builder": "@cypress/schematic:cypress", | ||||
| 					"options": { | ||||
| 						"watch": true, | ||||
| 						"headless": false | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|   | ||||
							
								
								
									
										9
									
								
								src-ui/cypress.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src-ui/cypress.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| { | ||||
|   "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" | ||||
| } | ||||
| @@ -0,0 +1 @@ | ||||
| {"count":27,"next":"http://localhost:8000/api/correspondents/?page=2","previous":null,"results":[{"id":9,"slug":"abc-test-correspondent","name":"ABC Test Correspondent","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":13,"slug":"corresp-10","name":"Corresp 10","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":14,"slug":"corresp-11","name":"Corresp 11","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":15,"slug":"corresp-12","name":"Corresp 12","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":16,"slug":"corresp-13","name":"Corresp 13","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":18,"slug":"corresp-15","name":"Corresp 15","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":19,"slug":"corresp-16","name":"Corresp 16","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":20,"slug":"corresp-17","name":"Corresp 17","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":21,"slug":"corresp-18","name":"Corresp 18","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":22,"slug":"corresp-19","name":"Corresp 19","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":23,"slug":"corresp-20","name":"Corresp 20","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":24,"slug":"corresp-21","name":"Corresp 21","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":25,"slug":"corresp-22","name":"Corresp 22","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":26,"slug":"corresp-23","name":"Corresp 23","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":5,"slug":"corresp-3","name":"Corresp 3","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":6,"slug":"corresp-4","name":"Corresp 4","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":7,"slug":"corresp-5","name":"Corresp 5","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":8,"slug":"corresp-6","name":"Corresp 6","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":10,"slug":"corresp-7","name":"Corresp 7","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":11,"slug":"corresp-8","name":"Corresp 8","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":12,"slug":"corresp-9","name":"Corresp 9","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":17,"slug":"correspondent-14","name":"Correspondent 14","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":2,"slug":"correspondent-2","name":"Correspondent 2","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":7,"last_correspondence":"2021-01-20T23:37:58.204614Z"},{"id":27,"slug":"michael-shamoon","name":"Michael Shamoon","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":1,"last_correspondence":"2022-03-16T03:48:50.089624Z"},{"id":4,"slug":"newest-correspondent","name":"Newest Correspondent","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":1,"last_correspondence":"2021-02-07T08:00:00Z"}]} | ||||
							
								
								
									
										1
									
								
								src-ui/cypress/fixtures/document_types/doctypes.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src-ui/cypress/fixtures/document_types/doctypes.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| {"count":1,"next":null,"previous":null,"results":[{"id":1,"slug":"test","name":"Test Doc Type","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0}]} | ||||
							
								
								
									
										1
									
								
								src-ui/cypress/fixtures/documents/1/metadata.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src-ui/cypress/fixtures/documents/1/metadata.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| {"original_checksum":"e959bc7d593245d92685213264e962ba","original_size":963754,"original_mime_type":"application/pdf","media_filename":"2022/lorem-ipsum.pdf","has_archive_version":true,"original_metadata":[],"archive_checksum":"5a1f46a9150bcade978c764b039ce4d0","archive_media_filename":"2022/lorem-ipsum.pdf","archive_size":351160,"archive_metadata":[{"namespace":"http://ns.adobe.com/pdf/1.3/","prefix":"pdf","key":"Producer","value":"pikepdf5.0.1"},{"namespace":"http://ns.adobe.com/xap/1.0/","prefix":"xmp","key":"ModifyDate","value":"2022-03-22T04:53:18+00:00"},{"namespace":"http://ns.adobe.com/xap/1.0/","prefix":"xmp","key":"CreateDate","value":"2022-03-22T18:05:43+00:00"},{"namespace":"http://ns.adobe.com/xap/1.0/","prefix":"xmp","key":"CreatorTool","value":"ocrmypdf13.4.0/TesseractOCR-PDF4.1.1"},{"namespace":"http://ns.adobe.com/xap/1.0/mm/","prefix":"xmpMM","key":"DocumentID","value":"uuid:df27edcf-e34a-11f7-0000-8fa6067a3c04"},{"namespace":"http://purl.org/dc/elements/1.1/","prefix":"dc","key":"format","value":"application/pdf"},{"namespace":"http://purl.org/dc/elements/1.1/","prefix":"dc","key":"title","value":"ScannedDocument"},{"namespace":"http://www.aiim.org/pdfa/ns/id/","prefix":"pdfaid","key":"part","value":"2"},{"namespace":"http://www.aiim.org/pdfa/ns/id/","prefix":"pdfaid","key":"conformance","value":"B"},{"namespace":"http://purl.org/dc/elements/1.1/","prefix":"dc","key":"creator","value":"None"},{"namespace":"http://ns.adobe.com/xap/1.0/","prefix":"xmp","key":"MetadataDate","value":"2022-03-22T21:53:18.882551-07:00"}]} | ||||
							
								
								
									
										1
									
								
								src-ui/cypress/fixtures/documents/1/suggestions.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src-ui/cypress/fixtures/documents/1/suggestions.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| {"correspondents":[],"tags":[3],"document_types":[1]} | ||||
							
								
								
									
										1
									
								
								src-ui/cypress/fixtures/documents/documents.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src-ui/cypress/fixtures/documents/documents.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								src-ui/cypress/fixtures/documents/lorem-ipsum.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src-ui/cypress/fixtures/documents/lorem-ipsum.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 156 KiB | 
							
								
								
									
										1
									
								
								src-ui/cypress/fixtures/saved_views/savedviews.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src-ui/cypress/fixtures/saved_views/savedviews.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| {"count":3,"next":null,"previous":null,"results":[{"id":1,"name":"Inbox","show_on_dashboard":true,"show_in_sidebar":true,"sort_field":"created","sort_reverse":true,"filter_rules":[{"rule_type":6,"value":"18"}]},{"id":2,"name":"Recently Added","show_on_dashboard":true,"show_in_sidebar":false,"sort_field":"created","sort_reverse":true,"filter_rules":[]},{"id":11,"name":"Taxes","show_on_dashboard":false,"show_in_sidebar":true,"sort_field":"created","sort_reverse":true,"filter_rules":[{"rule_type":6,"value":"39"}]}]} | ||||
							
								
								
									
										1
									
								
								src-ui/cypress/fixtures/tags/tags.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src-ui/cypress/fixtures/tags/tags.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| {"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}]} | ||||
							
								
								
									
										64
									
								
								src-ui/cypress/integration/document-detail.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src-ui/cypress/integration/document-detail.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| describe('document-detail', () => { | ||||
|   beforeEach(() => { | ||||
|     this.modifiedDocuments = [] | ||||
|  | ||||
|     cy.fixture('documents/documents.json').then((documentsJson) => { | ||||
|       cy.intercept('GET', 'http://localhost:8000/api/documents/1/', (req) => { | ||||
|         let response = { ...documentsJson } | ||||
|         response = response.results.find((d) => d.id == 1) | ||||
|         req.reply(response) | ||||
|       }) | ||||
|     }) | ||||
|  | ||||
|     cy.intercept('PUT', 'http://localhost:8000/api/documents/1/', (req) => { | ||||
|       this.modifiedDocuments.push(req.body) // store this for later | ||||
|       req.reply({ result: 'OK' }) | ||||
|     }).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.visit('/documents/1/') | ||||
|   }) | ||||
|  | ||||
|   it('should activate / deactivate save button when changes are saved', () => { | ||||
|     cy.contains('button', 'Save').should('be.disabled') | ||||
|     cy.get('app-input-text[formcontrolname="title"]') | ||||
|       .type(' additional') | ||||
|       .wait(1500) // this delay is for frontend debounce | ||||
|     cy.contains('button', 'Save').should('not.be.disabled') | ||||
|   }) | ||||
|  | ||||
|   it('should warn on unsaved changes', () => { | ||||
|     cy.get('app-input-text[formcontrolname="title"]') | ||||
|       .type(' additional') | ||||
|       .wait(1500) // this delay is for frontend debounce | ||||
|     cy.get('button[title="Close"]').click() | ||||
|     cy.contains('You have unsaved changes') | ||||
|     cy.contains('button', 'Cancel').click().wait(150) | ||||
|     cy.contains('button', 'Save').click().wait('@saveDoc').wait(2000) // navigates away after saving | ||||
|     cy.contains('You have unsaved changes').should('not.exist') | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										143
									
								
								src-ui/cypress/integration/documents-list.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								src-ui/cypress/integration/documents-list.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,143 @@ | ||||
| describe('documents-list', () => { | ||||
|   beforeEach(() => { | ||||
|     this.bulkEdits = {} | ||||
|  | ||||
|     // mock API methods | ||||
|     cy.fixture('documents/documents.json').then((documentsJson) => { | ||||
|       // bulk edit | ||||
|       cy.intercept( | ||||
|         'POST', | ||||
|         'http://localhost:8000/api/documents/bulk_edit/', | ||||
|         (req) => { | ||||
|           this.bulkEdits = req.body // store this for later | ||||
|           req.reply({ result: 'OK' }) | ||||
|         } | ||||
|       ) | ||||
|  | ||||
|       cy.intercept('GET', 'http://localhost:8000/api/documents/*', (req) => { | ||||
|         let response = { ...documentsJson } | ||||
|  | ||||
|         // bulkEdits was set earlier by bulk_edit intercept | ||||
|         if (this.bulkEdits.hasOwnProperty('documents')) { | ||||
|           response.results = response.results.map((d) => { | ||||
|             if ((this.bulkEdits['documents'] as Array<number>).includes(d.id)) { | ||||
|               switch (this.bulkEdits['method']) { | ||||
|                 case 'modify_tags': | ||||
|                   d.tags = (d.tags as Array<number>).concat([ | ||||
|                     this.bulkEdits['parameters']['add_tags'], | ||||
|                   ]) | ||||
|                   break | ||||
|                 case 'set_correspondent': | ||||
|                   d.correspondent = | ||||
|                     this.bulkEdits['parameters']['correspondent'] | ||||
|                   break | ||||
|                 case 'set_document_type': | ||||
|                   d.document_type = | ||||
|                     this.bulkEdits['parameters']['document_type'] | ||||
|                   break | ||||
|               } | ||||
|             } | ||||
|  | ||||
|             return d | ||||
|           }) | ||||
|         } else if (req.query.hasOwnProperty('tags__id__all')) { | ||||
|           // filtering e.g. http://localhost:8000/api/documents/?page=1&page_size=50&ordering=-created&tags__id__all=2 | ||||
|           const tag_id = +req.query['tags__id__all'] | ||||
|           response.results = (documentsJson.results as Array<any>).filter((d) => | ||||
|             (d.tags as Array<number>).includes(tag_id) | ||||
|           ) | ||||
|           response.count = response.results.length | ||||
|         } | ||||
|  | ||||
|         req.reply(response) | ||||
|       }) | ||||
|     }) | ||||
|  | ||||
|     cy.intercept('http://localhost:8000/api/documents/1/thumb/', { | ||||
|       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') | ||||
|   }) | ||||
|  | ||||
|   it('should show a list of documents rendered as cards with thumbnails', () => { | ||||
|     cy.contains('3 documents') | ||||
|     cy.contains('lorem-ipsum') | ||||
|     cy.get('app-document-card-small:first-of-type img') | ||||
|       .invoke('attr', 'src') | ||||
|       .should('eq', 'http://localhost:8000/api/documents/1/thumb/') | ||||
|   }) | ||||
|  | ||||
|   it('should change to table "details" view', () => { | ||||
|     cy.get('div.btn-group-toggle input[value="details"]').parent().click() | ||||
|     cy.get('table') | ||||
|   }) | ||||
|  | ||||
|   it('should change to large cards view', () => { | ||||
|     cy.get('div.btn-group-toggle input[value="largeCards"]').parent().click() | ||||
|     cy.get('app-document-card-large') | ||||
|   }) | ||||
|  | ||||
|   it('should filter tags', () => { | ||||
|     cy.get('app-filter-editor app-filterable-dropdown[title="Tags"]').within( | ||||
|       () => { | ||||
|         cy.contains('button', 'Tags').click() | ||||
|         cy.contains('button', 'Tag 2').click() | ||||
|       } | ||||
|     ) | ||||
|     cy.contains('One document') | ||||
|   }) | ||||
|  | ||||
|   it('should apply tags', () => { | ||||
|     cy.get('app-document-card-small:first-of-type').click() | ||||
|     cy.get('app-bulk-editor app-filterable-dropdown[title="Tags"]').within( | ||||
|       () => { | ||||
|         cy.contains('button', 'Tags').click() | ||||
|         cy.contains('button', 'Test Tag').click() | ||||
|         cy.contains('button', 'Apply').click() | ||||
|       } | ||||
|     ) | ||||
|     cy.contains('button', 'Confirm').click() | ||||
|     cy.get('app-document-card-small:first-of-type').contains('Test Tag') | ||||
|   }) | ||||
|  | ||||
|   it('should apply correspondent', () => { | ||||
|     cy.get('app-document-card-small:first-of-type').click() | ||||
|     cy.get( | ||||
|       'app-bulk-editor app-filterable-dropdown[title="Correspondent"]' | ||||
|     ).within(() => { | ||||
|       cy.contains('button', 'Correspondent').click() | ||||
|       cy.contains('button', 'ABC Test Correspondent').click() | ||||
|       cy.contains('button', 'Apply').click() | ||||
|     }) | ||||
|     cy.contains('button', 'Confirm').click() | ||||
|     cy.get('app-document-card-small:first-of-type').contains( | ||||
|       'ABC Test Correspondent' | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   it('should apply document type', () => { | ||||
|     cy.get('app-document-card-small:first-of-type').click() | ||||
|     cy.get( | ||||
|       'app-bulk-editor app-filterable-dropdown[title="Document type"]' | ||||
|     ).within(() => { | ||||
|       cy.contains('button', 'Document type').click() | ||||
|       cy.contains('button', 'Test Doc Type').click() | ||||
|       cy.contains('button', 'Apply').click() | ||||
|     }) | ||||
|     cy.contains('button', 'Confirm').click() | ||||
|     cy.get('app-document-card-small:first-of-type').contains('Test Doc Type') | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										32
									
								
								src-ui/cypress/integration/manage.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src-ui/cypress/integration/manage.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| describe('manage', () => { | ||||
|   beforeEach(() => { | ||||
|     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', () => { | ||||
|     cy.visit('/correspondents') | ||||
|     cy.get('tbody').find('tr').its('length').should('eq', 25) | ||||
|     cy.get('ngb-pagination').its('length').should('eq', 2) | ||||
|   }) | ||||
|  | ||||
|   it('should show a list of tags without bottom pagination', () => { | ||||
|     cy.visit('/tags') | ||||
|     cy.get('tbody').find('tr').its('length').should('eq', 8) | ||||
|     cy.get('ngb-pagination').its('length').should('eq', 1) | ||||
|   }) | ||||
|  | ||||
|   it('should show a list of documents filtered by tag', () => { | ||||
|     cy.intercept('http://localhost:8000/api/documents/*', (req) => { | ||||
|       if (req.url.indexOf('tags__id__all=4')) | ||||
|         req.reply({ count: 3, next: null, previous: null, results: [] }) | ||||
|     }) | ||||
|     cy.visit('/tags') | ||||
|     cy.get('tbody').find('button').contains('Documents').first().click() // id = 4 | ||||
|     cy.contains('3 documents') | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										91
									
								
								src-ui/cypress/integration/settings.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								src-ui/cypress/integration/settings.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,91 @@ | ||||
| describe('settings', () => { | ||||
|   beforeEach(() => { | ||||
|     this.modifiedViews = [] | ||||
|  | ||||
|     // mock API methods | ||||
|     cy.fixture('saved_views/savedviews.json').then((savedViewsJson) => { | ||||
|       // saved views PATCH | ||||
|       cy.intercept( | ||||
|         'PATCH', | ||||
|         'http://localhost:8000/api/saved_views/*', | ||||
|         (req) => { | ||||
|           this.modifiedViews.push(req.body) // store this for later | ||||
|           req.reply({ result: 'OK' }) | ||||
|         } | ||||
|       ) | ||||
|  | ||||
|       cy.intercept('GET', 'http://localhost:8000/api/saved_views/*', (req) => { | ||||
|         let response = { ...savedViewsJson } | ||||
|         if (this.modifiedViews.length) { | ||||
|           response.results = response.results.map((v) => { | ||||
|             if (this.modifiedViews.find((mv) => mv.id == v.id)) | ||||
|               v = this.modifiedViews.find((mv) => mv.id == v.id) | ||||
|             return v | ||||
|           }) | ||||
|         } | ||||
|  | ||||
|         req.reply(response) | ||||
|       }).as('savedViews') | ||||
|     }) | ||||
|  | ||||
|     cy.fixture('documents/documents.json').then((documentsJson) => { | ||||
|       cy.intercept('GET', 'http://localhost:8000/api/documents/1/', (req) => { | ||||
|         let response = { ...documentsJson } | ||||
|         response = response.results.find((d) => d.id == 1) | ||||
|         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.visit('/settings') | ||||
|     cy.wait('@savedViews') | ||||
|   }) | ||||
|  | ||||
|   it('should activate / deactivate save button when settings change and are saved', () => { | ||||
|     cy.contains('button', 'Save').should('be.disabled') | ||||
|     cy.contains('Use system settings').click() | ||||
|     cy.contains('button', 'Save').should('not.be.disabled') | ||||
|     cy.contains('button', 'Save').click() | ||||
|     cy.contains('button', 'Save').should('be.disabled') | ||||
|   }) | ||||
|  | ||||
|   it('should warn on unsaved changes', () => { | ||||
|     cy.contains('Use system settings').click() | ||||
|     cy.contains('a', 'Dashboard').click() | ||||
|     cy.contains('You have unsaved changes') | ||||
|     cy.contains('button', 'Cancel').click() | ||||
|     cy.contains('button', 'Save').click().wait('@savedViews') | ||||
|     cy.contains('a', 'Dashboard').click() | ||||
|     cy.contains('You have unsaved changes').should('not.exist') | ||||
|   }) | ||||
|  | ||||
|   it('should apply appearance changes when set', () => { | ||||
|     cy.contains('Use system settings').click() | ||||
|     cy.get('body').should('not.have.class', 'color-scheme-system') | ||||
|     cy.contains('Enable dark mode').click() | ||||
|     cy.get('body').should('have.class', 'color-scheme-dark') | ||||
|   }) | ||||
|  | ||||
|   it('should remove saved view from sidebar when unset', () => { | ||||
|     cy.contains('a', 'Saved views').click() | ||||
|     cy.get('#show_in_sidebar_1').click() | ||||
|     cy.contains('button', 'Save').click().wait('@savedViews') | ||||
|     cy.contains('li', 'Inbox').should('not.exist') | ||||
|   }) | ||||
|  | ||||
|   it('should remove saved view from dashboard when unset', () => { | ||||
|     cy.contains('a', 'Saved views').click() | ||||
|     cy.get('#show_on_dashboard_1').click() | ||||
|     cy.contains('button', 'Save').click().wait('@savedViews') | ||||
|     cy.visit('/dashboard') | ||||
|     cy.get('app-saved-view-widget').contains('Inbox').should('not.exist') | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										3
									
								
								src-ui/cypress/plugins/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src-ui/cypress/plugins/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| // Plugins enable you to tap into, modify, or extend the internal behavior of Cypress | ||||
| // For more info, visit https://on.cypress.io/plugins-api | ||||
| module.exports = (on, config) => {} | ||||
							
								
								
									
										43
									
								
								src-ui/cypress/support/commands.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src-ui/cypress/support/commands.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| // *********************************************** | ||||
| // This example namespace declaration will help | ||||
| // with Intellisense and code completion in your | ||||
| // IDE or Text Editor. | ||||
| // *********************************************** | ||||
| // declare namespace Cypress { | ||||
| //   interface Chainable<Subject = any> { | ||||
| //     customCommand(param: any): typeof customCommand; | ||||
| //   } | ||||
| // } | ||||
| // | ||||
| // function customCommand(param: any): void { | ||||
| //   console.warn(param); | ||||
| // } | ||||
| // | ||||
| // NOTE: You can use it like so: | ||||
| // Cypress.Commands.add('customCommand', customCommand); | ||||
| // | ||||
| // *********************************************** | ||||
| // This example commands.js shows you how to | ||||
| // create various custom commands and overwrite | ||||
| // existing commands. | ||||
| // | ||||
| // For more comprehensive examples of custom | ||||
| // commands please read more here: | ||||
| // https://on.cypress.io/custom-commands | ||||
| // *********************************************** | ||||
| // | ||||
| // | ||||
| // -- This is a parent command -- | ||||
| // Cypress.Commands.add("login", (email, password) => { ... }) | ||||
| // | ||||
| // | ||||
| // -- This is a child command -- | ||||
| // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) | ||||
| // | ||||
| // | ||||
| // -- This is a dual command -- | ||||
| // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) | ||||
| // | ||||
| // | ||||
| // -- This will overwrite an existing command -- | ||||
| // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) | ||||
							
								
								
									
										17
									
								
								src-ui/cypress/support/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src-ui/cypress/support/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| // *********************************************************** | ||||
| // 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'; | ||||
							
								
								
									
										8
									
								
								src-ui/cypress/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src-ui/cypress/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| { | ||||
|   "extends": "../tsconfig.json", | ||||
|   "include": ["**/*.ts"], | ||||
|   "compilerOptions": { | ||||
|     "sourceMap": false, | ||||
|     "types": ["cypress"] | ||||
|   } | ||||
| } | ||||
| @@ -2,18 +2,16 @@ | ||||
| // Protractor configuration file, see link for more information | ||||
| // https://github.com/angular/protractor/blob/master/lib/config.ts | ||||
|  | ||||
| const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter'); | ||||
| const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter') | ||||
|  | ||||
| /** | ||||
|  * @type { import("protractor").Config } | ||||
|  */ | ||||
| exports.config = { | ||||
|   allScriptsTimeout: 11000, | ||||
|   specs: [ | ||||
|     './src/**/*.e2e-spec.ts' | ||||
|   ], | ||||
|   specs: ['./src/**/*.e2e-spec.ts'], | ||||
|   capabilities: { | ||||
|     browserName: 'chrome' | ||||
|     browserName: 'chrome', | ||||
|   }, | ||||
|   directConnect: true, | ||||
|   baseUrl: 'http://localhost:4200/', | ||||
| @@ -21,16 +19,18 @@ exports.config = { | ||||
|   jasmineNodeOpts: { | ||||
|     showColors: true, | ||||
|     defaultTimeoutInterval: 30000, | ||||
|     print: function() {} | ||||
|     print: function () {}, | ||||
|   }, | ||||
|   onPrepare() { | ||||
|     require('ts-node').register({ | ||||
|       project: require('path').join(__dirname, './tsconfig.json') | ||||
|     }); | ||||
|     jasmine.getEnv().addReporter(new SpecReporter({ | ||||
|       spec: { | ||||
|         displayStacktrace: StacktraceOption.PRETTY | ||||
|       } | ||||
|     })); | ||||
|   } | ||||
| }; | ||||
|       project: require('path').join(__dirname, './tsconfig.json'), | ||||
|     }) | ||||
|     jasmine.getEnv().addReporter( | ||||
|       new SpecReporter({ | ||||
|         spec: { | ||||
|           displayStacktrace: StacktraceOption.PRETTY, | ||||
|         }, | ||||
|       }) | ||||
|     ) | ||||
|   }, | ||||
| } | ||||
|   | ||||
| @@ -1,23 +1,25 @@ | ||||
| import { AppPage } from './app.po'; | ||||
| import { browser, logging } from 'protractor'; | ||||
| import { AppPage } from './app.po' | ||||
| import { browser, logging } from 'protractor' | ||||
|  | ||||
| describe('workspace-project App', () => { | ||||
|   let page: AppPage; | ||||
|   let page: AppPage | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     page = new AppPage(); | ||||
|   }); | ||||
|     page = new AppPage() | ||||
|   }) | ||||
|  | ||||
|   it('should display welcome message', () => { | ||||
|     page.navigateTo(); | ||||
|     expect(page.getTitleText()).toEqual('paperless-ui app is running!'); | ||||
|   }); | ||||
|     page.navigateTo() | ||||
|     expect(page.getTitleText()).toEqual('paperless-ui app is running!') | ||||
|   }) | ||||
|  | ||||
|   afterEach(async () => { | ||||
|     // Assert that there are no errors emitted from the browser | ||||
|     const logs = await browser.manage().logs().get(logging.Type.BROWSER); | ||||
|     expect(logs).not.toContain(jasmine.objectContaining({ | ||||
|       level: logging.Level.SEVERE, | ||||
|     } as logging.Entry)); | ||||
|   }); | ||||
| }); | ||||
|     const logs = await browser.manage().logs().get(logging.Type.BROWSER) | ||||
|     expect(logs).not.toContain( | ||||
|       jasmine.objectContaining({ | ||||
|         level: logging.Level.SEVERE, | ||||
|       } as logging.Entry) | ||||
|     ) | ||||
|   }) | ||||
| }) | ||||
|   | ||||
| @@ -1,11 +1,13 @@ | ||||
| import { browser, by, element } from 'protractor'; | ||||
| import { browser, by, element } from 'protractor' | ||||
|  | ||||
| export class AppPage { | ||||
|   navigateTo(): Promise<unknown> { | ||||
|     return browser.get(browser.baseUrl) as Promise<unknown>; | ||||
|     return browser.get(browser.baseUrl) as Promise<unknown> | ||||
|   } | ||||
|  | ||||
|   getTitleText(): Promise<string> { | ||||
|     return element(by.css('app-root .content span')).getText() as Promise<string>; | ||||
|     return element( | ||||
|       by.css('app-root .content span') | ||||
|     ).getText() as Promise<string> | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										8
									
								
								src-ui/jest.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src-ui/jest.config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| module.exports = { | ||||
|   moduleNameMapper: { | ||||
|     '@core/(.*)': '<rootDir>/src/app/core/$1', | ||||
|   }, | ||||
|   preset: 'jest-preset-angular', | ||||
|   setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'], | ||||
|   testPathIgnorePatterns: ['/node_modules/', '/cypress/'], | ||||
| } | ||||
| @@ -1,32 +0,0 @@ | ||||
| // Karma configuration file, see link for more information | ||||
| // https://karma-runner.github.io/1.0/config/configuration-file.html | ||||
|  | ||||
| module.exports = function (config) { | ||||
|   config.set({ | ||||
|     basePath: '', | ||||
|     frameworks: ['jasmine', '@angular-devkit/build-angular'], | ||||
|     plugins: [ | ||||
|       require('karma-jasmine'), | ||||
|       require('karma-chrome-launcher'), | ||||
|       require('karma-jasmine-html-reporter'), | ||||
|       require('karma-coverage-istanbul-reporter'), | ||||
|       require('@angular-devkit/build-angular/plugins/karma') | ||||
|     ], | ||||
|     client: { | ||||
|       clearContext: false // leave Jasmine Spec Runner output visible in browser | ||||
|     }, | ||||
|     coverageIstanbulReporter: { | ||||
|       dir: require('path').join(__dirname, './coverage/paperless-ui'), | ||||
|       reports: ['html', 'lcovonly', 'text-summary'], | ||||
|       fixWebpackSourcePaths: true | ||||
|     }, | ||||
|     reporters: ['progress', 'kjhtml'], | ||||
|     port: 9876, | ||||
|     colors: true, | ||||
|     logLevel: config.LOG_INFO, | ||||
|     autoWatch: true, | ||||
|     browsers: ['Chrome'], | ||||
|     singleRun: false, | ||||
|     restartOnFileChange: true | ||||
|   }); | ||||
| }; | ||||
							
								
								
									
										14809
									
								
								src-ui/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14809
									
								
								src-ui/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -7,53 +7,52 @@ | ||||
|     "build": "ng build", | ||||
|     "test": "ng test", | ||||
|     "lint": "ng lint", | ||||
|     "e2e": "ng e2e" | ||||
|     "e2e": "ng e2e", | ||||
|     "cy:run": "cypress run", | ||||
|     "e2e:ci": "concurrently 'npm run start' 'wait-on http-get://localhost:4200 && npm run cy:run' --kill-others --success first" | ||||
|   }, | ||||
|   "private": true, | ||||
|   "dependencies": { | ||||
|     "@angular/animations": "~13.2.4", | ||||
|     "@angular/common": "~13.2.5", | ||||
|     "@angular/compiler": "~13.2.4", | ||||
|     "@angular/core": "~13.2.4", | ||||
|     "@angular/forms": "~13.2.5", | ||||
|     "@angular/localize": "~13.2.4", | ||||
|     "@angular/platform-browser": "~13.2.5", | ||||
|     "@angular/platform-browser-dynamic": "~13.2.4", | ||||
|     "@angular/router": "~13.2.5", | ||||
|     "@ng-bootstrap/ng-bootstrap": "^12.0.0", | ||||
|     "@angular/common": "~13.3.1", | ||||
|     "@angular/compiler": "~13.3.1", | ||||
|     "@angular/core": "~13.3.1", | ||||
|     "@angular/forms": "~13.3.1", | ||||
|     "@angular/localize": "~13.3.1", | ||||
|     "@angular/platform-browser": "~13.3.1", | ||||
|     "@angular/platform-browser-dynamic": "~13.3.1", | ||||
|     "@angular/router": "~13.3.1", | ||||
|     "@ng-bootstrap/ng-bootstrap": "^12.0.1", | ||||
|     "@ng-select/ng-select": "^8.1.1", | ||||
|     "@ngneat/dirty-check-forms": "^1.1.0", | ||||
|     "@popperjs/core": "^2.11.2", | ||||
|     "@ngneat/dirty-check-forms": "^3.0.2", | ||||
|     "@popperjs/core": "^2.11.4", | ||||
|     "bootstrap": "^5.1.3", | ||||
|     "file-saver": "^2.0.5", | ||||
|     "ng2-pdf-viewer": "^8.0.1", | ||||
|     "ng2-pdf-viewer": "^9.0.0", | ||||
|     "ngx-color": "^7.3.3", | ||||
|     "ngx-cookie-service": "^13.1.2", | ||||
|     "ngx-file-drop": "^13.0.0", | ||||
|     "ngx-infinite-scroll": "^10.0.1", | ||||
|     "rxjs": "~6.6.7", | ||||
|     "rxjs": "~7.5.5", | ||||
|     "tslib": "^2.3.1", | ||||
|     "uuid": "^8.3.1", | ||||
|     "zone.js": "~0.11.4" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@angular-devkit/build-angular": "~13.2.5", | ||||
|     "@angular/cli": "~13.2.5", | ||||
|     "@angular/compiler-cli": "~13.2.4", | ||||
|     "@types/jasmine": "~3.10.3", | ||||
|     "@types/jasminewd2": "~2.0.10", | ||||
|     "@types/node": "^17.0.21", | ||||
|     "@angular-builders/jest": "13.0.3", | ||||
|     "@angular-devkit/build-angular": "~13.3.1", | ||||
|     "@angular/cli": "~13.3.1", | ||||
|     "@angular/compiler-cli": "~13.3.1", | ||||
|     "@types/jest": "27.4.1", | ||||
|     "@types/node": "^17.0.23", | ||||
|     "codelyzer": "^6.0.2", | ||||
|     "jasmine-core": "~4.0.1", | ||||
|     "jasmine-spec-reporter": "~7.0.0", | ||||
|     "karma": "~6.3.16", | ||||
|     "karma-chrome-launcher": "~3.1.0", | ||||
|     "karma-coverage-istanbul-reporter": "~3.0.3", | ||||
|     "karma-jasmine": "~4.0.1", | ||||
|     "karma-jasmine-html-reporter": "^1.7.0", | ||||
|     "protractor": "~7.0.0", | ||||
|     "concurrently": "7.0.0", | ||||
|     "jest": "27.5.1", | ||||
|     "ts-node": "~10.7.0", | ||||
|     "tslint": "~6.1.3", | ||||
|     "typescript": "~4.5.5" | ||||
|     "typescript": "~4.6.3", | ||||
|     "wait-on": "~6.0.1" | ||||
|   }, | ||||
|   "optionalDependencies": { | ||||
|     "cypress": "~9.5.3", | ||||
|     "@cypress/schematic": "^1.6.0" | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										30
									
								
								src-ui/setup-jest.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src-ui/setup-jest.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| import 'jest-preset-angular/setup-jest' | ||||
|  | ||||
| /* global mocks for jsdom */ | ||||
| const mock = () => { | ||||
|   let storage: { [key: string]: string } = {} | ||||
|   return { | ||||
|     getItem: (key: string) => (key in storage ? storage[key] : null), | ||||
|     setItem: (key: string, value: string) => (storage[key] = value || ''), | ||||
|     removeItem: (key: string) => delete storage[key], | ||||
|     clear: () => (storage = {}), | ||||
|   } | ||||
| } | ||||
|  | ||||
| Object.defineProperty(window, 'localStorage', { value: mock() }) | ||||
| Object.defineProperty(window, 'sessionStorage', { value: mock() }) | ||||
| Object.defineProperty(window, 'getComputedStyle', { | ||||
|   value: () => ['-webkit-appearance'], | ||||
| }) | ||||
|  | ||||
| Object.defineProperty(document.body.style, 'transform', { | ||||
|   value: () => { | ||||
|     return { | ||||
|       enumerable: true, | ||||
|       configurable: true, | ||||
|     } | ||||
|   }, | ||||
| }) | ||||
|  | ||||
| /* output shorter and more meaningful Zone error stack traces */ | ||||
| // Error.stackTraceLimit = 2 | ||||
| @@ -1,39 +1,47 @@ | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { Routes, RouterModule } from '@angular/router'; | ||||
| import { AppFrameComponent } from './components/app-frame/app-frame.component'; | ||||
| import { DashboardComponent } from './components/dashboard/dashboard.component'; | ||||
| import { DocumentDetailComponent } from './components/document-detail/document-detail.component'; | ||||
| import { DocumentListComponent } from './components/document-list/document-list.component'; | ||||
| import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'; | ||||
| import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'; | ||||
| import { LogsComponent } from './components/manage/logs/logs.component'; | ||||
| import { SettingsComponent } from './components/manage/settings/settings.component'; | ||||
| import { TagListComponent } from './components/manage/tag-list/tag-list.component'; | ||||
| import { NotFoundComponent } from './components/not-found/not-found.component'; | ||||
| import {DocumentAsnComponent} from "./components/document-asn/document-asn.component"; | ||||
| import { DirtyFormGuard } from './guards/dirty-form.guard'; | ||||
| import { NgModule } from '@angular/core' | ||||
| import { Routes, RouterModule } from '@angular/router' | ||||
| import { AppFrameComponent } from './components/app-frame/app-frame.component' | ||||
| import { DashboardComponent } from './components/dashboard/dashboard.component' | ||||
| import { DocumentDetailComponent } from './components/document-detail/document-detail.component' | ||||
| import { DocumentListComponent } from './components/document-list/document-list.component' | ||||
| import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component' | ||||
| import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component' | ||||
| import { LogsComponent } from './components/manage/logs/logs.component' | ||||
| import { SettingsComponent } from './components/manage/settings/settings.component' | ||||
| import { TagListComponent } from './components/manage/tag-list/tag-list.component' | ||||
| import { NotFoundComponent } from './components/not-found/not-found.component' | ||||
| import { DocumentAsnComponent } from './components/document-asn/document-asn.component' | ||||
| import { DirtyFormGuard } from './guards/dirty-form.guard' | ||||
|  | ||||
| const routes: Routes = [ | ||||
|   {path: '', redirectTo: 'dashboard', pathMatch: 'full'}, | ||||
|   {path: '', component: AppFrameComponent, children: [ | ||||
|     {path: 'dashboard', component: DashboardComponent }, | ||||
|     {path: 'documents', component: DocumentListComponent }, | ||||
|     {path: 'view/:id', component: DocumentListComponent }, | ||||
|     {path: 'documents/:id', component: DocumentDetailComponent }, | ||||
|     {path: 'asn/:id', component: DocumentAsnComponent }, | ||||
|     {path: 'tags', component: TagListComponent }, | ||||
|     {path: 'documenttypes', component: DocumentTypeListComponent }, | ||||
|     {path: 'correspondents', component: CorrespondentListComponent }, | ||||
|     {path: 'logs', component: LogsComponent }, | ||||
|     {path: 'settings', component: SettingsComponent, canDeactivate: [DirtyFormGuard] }, | ||||
|   ]}, | ||||
|   { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, | ||||
|   { | ||||
|     path: '', | ||||
|     component: AppFrameComponent, | ||||
|     children: [ | ||||
|       { path: 'dashboard', component: DashboardComponent }, | ||||
|       { path: 'documents', component: DocumentListComponent }, | ||||
|       { path: 'view/:id', component: DocumentListComponent }, | ||||
|       { path: 'documents/:id', component: DocumentDetailComponent }, | ||||
|       { path: 'asn/:id', component: DocumentAsnComponent }, | ||||
|       { path: 'tags', component: TagListComponent }, | ||||
|       { path: 'documenttypes', component: DocumentTypeListComponent }, | ||||
|       { path: 'correspondents', component: CorrespondentListComponent }, | ||||
|       { path: 'logs', component: LogsComponent }, | ||||
|       { | ||||
|         path: 'settings', | ||||
|         component: SettingsComponent, | ||||
|         canDeactivate: [DirtyFormGuard], | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|  | ||||
|   {path: '404', component: NotFoundComponent}, | ||||
|   {path: '**', redirectTo: '/404', pathMatch: 'full'} | ||||
| ]; | ||||
|   { path: '404', component: NotFoundComponent }, | ||||
|   { path: '**', redirectTo: '/404', pathMatch: 'full' }, | ||||
| ] | ||||
|  | ||||
| @NgModule({ | ||||
|   imports: [RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy' })], | ||||
|   exports: [RouterModule] | ||||
|   exports: [RouterModule], | ||||
| }) | ||||
| export class AppRoutingModule { } | ||||
| export class AppRoutingModule {} | ||||
|   | ||||
| @@ -1,3 +1,13 @@ | ||||
| <app-toasts></app-toasts> | ||||
|  | ||||
| <router-outlet></router-outlet> | ||||
| <ngx-file-drop dropZoneClassName="main-dropzone" contentClassName="main-content" [disabled]="!dragDropEnabled" | ||||
| (onFileDrop)="dropped($event)" (onFileOver)="fileOver()" (onFileLeave)="fileLeave()"> | ||||
|     <ng-template ngx-file-drop-content-tmp> | ||||
|         <div class="global-dropzone-overlay fade" [class.show]="fileIsOver" [class.hide]="hidden"> | ||||
|             <h2 i18n>Drop files to begin upload</h2> | ||||
|         </div> | ||||
|         <div [class.inert]="fileIsOver"> | ||||
|             <router-outlet></router-outlet> | ||||
|         </div> | ||||
|     </ng-template> | ||||
| </ngx-file-drop> | ||||
|   | ||||
| @@ -1,35 +0,0 @@ | ||||
| import { TestBed } from '@angular/core/testing'; | ||||
| import { RouterTestingModule } from '@angular/router/testing'; | ||||
| import { AppComponent } from './app.component'; | ||||
|  | ||||
| describe('AppComponent', () => { | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       imports: [ | ||||
|         RouterTestingModule | ||||
|       ], | ||||
|       declarations: [ | ||||
|         AppComponent | ||||
|       ], | ||||
|     }).compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   it('should create the app', () => { | ||||
|     const fixture = TestBed.createComponent(AppComponent); | ||||
|     const app = fixture.componentInstance; | ||||
|     expect(app).toBeTruthy(); | ||||
|   }); | ||||
|  | ||||
|   it(`should have as title 'paperless-ui'`, () => { | ||||
|     const fixture = TestBed.createComponent(AppComponent); | ||||
|     const app = fixture.componentInstance; | ||||
|     expect(app.title).toEqual('paperless-ui'); | ||||
|   }); | ||||
|  | ||||
|   it('should render title', () => { | ||||
|     const fixture = TestBed.createComponent(AppComponent); | ||||
|     fixture.detectChanges(); | ||||
|     const compiled = fixture.nativeElement; | ||||
|     expect(compiled.querySelector('.content span').textContent).toContain('paperless-ui app is running!'); | ||||
|   }); | ||||
| }); | ||||
| @@ -1,25 +1,36 @@ | ||||
| import { SettingsService, SETTINGS_KEYS } from './services/settings.service'; | ||||
| import { Component, OnDestroy, OnInit } from '@angular/core'; | ||||
| import { Router } from '@angular/router'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| import { ConsumerStatusService } from './services/consumer-status.service'; | ||||
| import { ToastService } from './services/toast.service'; | ||||
| import { SettingsService, SETTINGS_KEYS } from './services/settings.service' | ||||
| import { Component, OnDestroy, OnInit } from '@angular/core' | ||||
| import { Router } from '@angular/router' | ||||
| import { Subscription } from 'rxjs' | ||||
| import { ConsumerStatusService } from './services/consumer-status.service' | ||||
| import { ToastService } from './services/toast.service' | ||||
| import { NgxFileDropEntry } from 'ngx-file-drop' | ||||
| import { UploadDocumentsService } from './services/upload-documents.service' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-root', | ||||
|   templateUrl: './app.component.html', | ||||
|   styleUrls: ['./app.component.scss'] | ||||
|   styleUrls: ['./app.component.scss'], | ||||
| }) | ||||
| export class AppComponent implements OnInit, OnDestroy { | ||||
|   newDocumentSubscription: Subscription | ||||
|   successSubscription: Subscription | ||||
|   failedSubscription: Subscription | ||||
|  | ||||
|   newDocumentSubscription: Subscription; | ||||
|   successSubscription: Subscription; | ||||
|   failedSubscription: Subscription; | ||||
|   private fileLeaveTimeoutID: any | ||||
|   fileIsOver: boolean = false | ||||
|   hidden: boolean = true | ||||
|  | ||||
|   constructor (private settings: SettingsService, private consumerStatusService: ConsumerStatusService, private toastService: ToastService, private router: Router) { | ||||
|     let anyWindow = (window as any) | ||||
|     anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.js'; | ||||
|     this.settings.updateDarkModeSettings() | ||||
|   constructor( | ||||
|     private settings: SettingsService, | ||||
|     private consumerStatusService: ConsumerStatusService, | ||||
|     private toastService: ToastService, | ||||
|     private router: Router, | ||||
|     private uploadDocumentsService: UploadDocumentsService | ||||
|   ) { | ||||
|     let anyWindow = window as any | ||||
|     anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.js' | ||||
|     this.settings.updateAppearanceSettings() | ||||
|   } | ||||
|  | ||||
|   ngOnDestroy(): void { | ||||
| @@ -36,7 +47,12 @@ export class AppComponent implements OnInit, OnDestroy { | ||||
|   } | ||||
|  | ||||
|   private showNotification(key) { | ||||
|     if (this.router.url == '/dashboard' && this.settings.get(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD)) { | ||||
|     if ( | ||||
|       this.router.url == '/dashboard' && | ||||
|       this.settings.get( | ||||
|         SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD | ||||
|       ) | ||||
|     ) { | ||||
|       return false | ||||
|     } | ||||
|     return this.settings.get(key) | ||||
| @@ -45,26 +61,82 @@ export class AppComponent implements OnInit, OnDestroy { | ||||
|   ngOnInit(): void { | ||||
|     this.consumerStatusService.connect() | ||||
|  | ||||
|     this.successSubscription = this.consumerStatusService | ||||
|       .onDocumentConsumptionFinished() | ||||
|       .subscribe((status) => { | ||||
|         if ( | ||||
|           this.showNotification(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS) | ||||
|         ) { | ||||
|           this.toastService.show({ | ||||
|             title: $localize`Document added`, | ||||
|             delay: 10000, | ||||
|             content: $localize`Document ${status.filename} was added to paperless.`, | ||||
|             actionName: $localize`Open document`, | ||||
|             action: () => { | ||||
|               this.router.navigate(['documents', status.documentId]) | ||||
|             }, | ||||
|           }) | ||||
|         } | ||||
|       }) | ||||
|  | ||||
|     this.successSubscription = this.consumerStatusService.onDocumentConsumptionFinished().subscribe(status => { | ||||
|       if (this.showNotification(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS)) { | ||||
|         this.toastService.show({title: $localize`Document added`, delay: 10000, content: $localize`Document ${status.filename} was added to paperless.`, actionName: $localize`Open document`, action: () => { | ||||
|           this.router.navigate(['documents', status.documentId]) | ||||
|         }}) | ||||
|       } | ||||
|     }) | ||||
|     this.failedSubscription = this.consumerStatusService | ||||
|       .onDocumentConsumptionFailed() | ||||
|       .subscribe((status) => { | ||||
|         if ( | ||||
|           this.showNotification(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_FAILED) | ||||
|         ) { | ||||
|           this.toastService.showError( | ||||
|             $localize`Could not add ${status.filename}\: ${status.message}` | ||||
|           ) | ||||
|         } | ||||
|       }) | ||||
|  | ||||
|     this.failedSubscription = this.consumerStatusService.onDocumentConsumptionFailed().subscribe(status => { | ||||
|       if (this.showNotification(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_FAILED)) { | ||||
|         this.toastService.showError($localize`Could not add ${status.filename}\: ${status.message}`) | ||||
|       } | ||||
|     }) | ||||
|  | ||||
|     this.newDocumentSubscription = this.consumerStatusService.onDocumentDetected().subscribe(status => { | ||||
|       if (this.showNotification(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT)) { | ||||
|         this.toastService.show({title: $localize`New document detected`, delay: 5000, content: $localize`Document ${status.filename} is being processed by paperless.`}) | ||||
|       } | ||||
|     }) | ||||
|     this.newDocumentSubscription = this.consumerStatusService | ||||
|       .onDocumentDetected() | ||||
|       .subscribe((status) => { | ||||
|         if ( | ||||
|           this.showNotification( | ||||
|             SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT | ||||
|           ) | ||||
|         ) { | ||||
|           this.toastService.show({ | ||||
|             title: $localize`New document detected`, | ||||
|             delay: 5000, | ||||
|             content: $localize`Document ${status.filename} is being processed by paperless.`, | ||||
|           }) | ||||
|         } | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   public get dragDropEnabled(): boolean { | ||||
|     return !this.router.url.includes('dashboard') | ||||
|   } | ||||
|  | ||||
|   public fileOver() { | ||||
|     // allows transition | ||||
|     setTimeout(() => { | ||||
|       this.fileIsOver = true | ||||
|     }, 1) | ||||
|     this.hidden = false | ||||
|     // stop fileLeave timeout | ||||
|     clearTimeout(this.fileLeaveTimeoutID) | ||||
|   } | ||||
|  | ||||
|   public fileLeave(immediate: boolean = false) { | ||||
|     const ms = immediate ? 0 : 500 | ||||
|  | ||||
|     this.fileLeaveTimeoutID = setTimeout(() => { | ||||
|       this.fileIsOver = false | ||||
|       // await transition completed | ||||
|       setTimeout(() => { | ||||
|         this.hidden = true | ||||
|       }, 150) | ||||
|     }, ms) | ||||
|   } | ||||
|  | ||||
|   public dropped(files: NgxFileDropEntry[]) { | ||||
|     this.fileLeave(true) | ||||
|     this.uploadDocumentsService.uploadFiles(files) | ||||
|     this.toastService.showInfo($localize`Initiating upload...`, 3000) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,86 +1,94 @@ | ||||
| import { BrowserModule } from '@angular/platform-browser'; | ||||
| import { NgModule } from '@angular/core'; | ||||
|  | ||||
| import { AppRoutingModule } from './app-routing.module'; | ||||
| import { AppComponent } from './app.component'; | ||||
| import { NgbDateAdapter, NgbDateParserFormatter, NgbModule } from '@ng-bootstrap/ng-bootstrap'; | ||||
| import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; | ||||
| import { DocumentListComponent } from './components/document-list/document-list.component'; | ||||
| import { DocumentDetailComponent } from './components/document-detail/document-detail.component'; | ||||
| import { DashboardComponent } from './components/dashboard/dashboard.component'; | ||||
| import { TagListComponent } from './components/manage/tag-list/tag-list.component'; | ||||
| import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'; | ||||
| import { LogsComponent } from './components/manage/logs/logs.component'; | ||||
| import { SettingsComponent } from './components/manage/settings/settings.component'; | ||||
| import { FormsModule, ReactiveFormsModule } from '@angular/forms'; | ||||
| import { DatePipe, registerLocaleData } from '@angular/common'; | ||||
| import { NotFoundComponent } from './components/not-found/not-found.component'; | ||||
| import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'; | ||||
| import { ConfirmDialogComponent } from './components/common/confirm-dialog/confirm-dialog.component'; | ||||
| import { CorrespondentEditDialogComponent } from './components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component'; | ||||
| import { TagEditDialogComponent } from './components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component'; | ||||
| import { DocumentTypeEditDialogComponent } from './components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component'; | ||||
| import { TagComponent } from './components/common/tag/tag.component'; | ||||
| import { PageHeaderComponent } from './components/common/page-header/page-header.component'; | ||||
| import { AppFrameComponent } from './components/app-frame/app-frame.component'; | ||||
| import { ToastsComponent } from './components/common/toasts/toasts.component'; | ||||
| import { FilterEditorComponent } from './components/document-list/filter-editor/filter-editor.component'; | ||||
| import { FilterableDropdownComponent } from './components/common/filterable-dropdown/filterable-dropdown.component'; | ||||
| import { ToggleableDropdownButtonComponent } from './components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component'; | ||||
| import { DateDropdownComponent } from './components/common/date-dropdown/date-dropdown.component'; | ||||
| import { DocumentCardLargeComponent } from './components/document-list/document-card-large/document-card-large.component'; | ||||
| import { DocumentCardSmallComponent } from './components/document-list/document-card-small/document-card-small.component'; | ||||
| import { BulkEditorComponent } from './components/document-list/bulk-editor/bulk-editor.component'; | ||||
| import { NgxFileDropModule } from 'ngx-file-drop'; | ||||
| import { TextComponent } from './components/common/input/text/text.component'; | ||||
| import { SelectComponent } from './components/common/input/select/select.component'; | ||||
| import { CheckComponent } from './components/common/input/check/check.component'; | ||||
| import { SaveViewConfigDialogComponent } from './components/document-list/save-view-config-dialog/save-view-config-dialog.component'; | ||||
| import { InfiniteScrollModule } from 'ngx-infinite-scroll'; | ||||
| import { TagsComponent } from './components/common/input/tags/tags.component'; | ||||
| import { SortableDirective } from './directives/sortable.directive'; | ||||
| import { CookieService } from 'ngx-cookie-service'; | ||||
| import { CsrfInterceptor } from './interceptors/csrf.interceptor'; | ||||
| import { SavedViewWidgetComponent } from './components/dashboard/widgets/saved-view-widget/saved-view-widget.component'; | ||||
| import { StatisticsWidgetComponent } from './components/dashboard/widgets/statistics-widget/statistics-widget.component'; | ||||
| import { UploadFileWidgetComponent } from './components/dashboard/widgets/upload-file-widget/upload-file-widget.component'; | ||||
| import { WidgetFrameComponent } from './components/dashboard/widgets/widget-frame/widget-frame.component'; | ||||
| import { PdfViewerModule } from 'ng2-pdf-viewer'; | ||||
| import { WelcomeWidgetComponent } from './components/dashboard/widgets/welcome-widget/welcome-widget.component'; | ||||
| import { YesNoPipe } from './pipes/yes-no.pipe'; | ||||
| import { FileSizePipe } from './pipes/file-size.pipe'; | ||||
| import { FilterPipe } from './pipes/filter.pipe'; | ||||
| import { DocumentTitlePipe } from './pipes/document-title.pipe'; | ||||
| import { MetadataCollapseComponent } from './components/document-detail/metadata-collapse/metadata-collapse.component'; | ||||
| import { SelectDialogComponent } from './components/common/select-dialog/select-dialog.component'; | ||||
| import { NgSelectModule } from '@ng-select/ng-select'; | ||||
| import { NumberComponent } from './components/common/input/number/number.component'; | ||||
| import { SafePipe } from './pipes/safe.pipe'; | ||||
| import { CustomDatePipe } from './pipes/custom-date.pipe'; | ||||
| import { DateComponent } from './components/common/input/date/date.component'; | ||||
| import { ISODateTimeAdapter } from './utils/ngb-iso-date-time-adapter'; | ||||
| import { LocalizedDateParserFormatter } from './utils/ngb-date-parser-formatter'; | ||||
| import { ApiVersionInterceptor } from './interceptors/api-version.interceptor'; | ||||
| import { ColorSliderModule } from 'ngx-color/slider'; | ||||
| import { ColorComponent } from './components/common/input/color/color.component'; | ||||
| import { DocumentAsnComponent } from './components/document-asn/document-asn.component'; | ||||
|  | ||||
| import localeCs from '@angular/common/locales/cs'; | ||||
| import localeDa from '@angular/common/locales/da'; | ||||
| import localeDe from '@angular/common/locales/de'; | ||||
| import localeEnGb from '@angular/common/locales/en-GB'; | ||||
| import localeEs from '@angular/common/locales/es'; | ||||
| import localeFr from '@angular/common/locales/fr'; | ||||
| import localeIt from '@angular/common/locales/it'; | ||||
| import localeLb from '@angular/common/locales/lb'; | ||||
| import localeNl from '@angular/common/locales/nl'; | ||||
| import localePl from '@angular/common/locales/pl'; | ||||
| import localePt from '@angular/common/locales/pt'; | ||||
| import localeSv from '@angular/common/locales/sv'; | ||||
| import localeRo from '@angular/common/locales/ro'; | ||||
| import localeRu from '@angular/common/locales/ru'; | ||||
| import { BrowserModule } from '@angular/platform-browser' | ||||
| import { NgModule } from '@angular/core' | ||||
| import { AppRoutingModule } from './app-routing.module' | ||||
| import { AppComponent } from './app.component' | ||||
| import { | ||||
|   NgbDateAdapter, | ||||
|   NgbDateParserFormatter, | ||||
|   NgbModule, | ||||
| } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http' | ||||
| import { DocumentListComponent } from './components/document-list/document-list.component' | ||||
| import { DocumentDetailComponent } from './components/document-detail/document-detail.component' | ||||
| import { DashboardComponent } from './components/dashboard/dashboard.component' | ||||
| import { TagListComponent } from './components/manage/tag-list/tag-list.component' | ||||
| import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component' | ||||
| import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component' | ||||
| import { LogsComponent } from './components/manage/logs/logs.component' | ||||
| import { SettingsComponent } from './components/manage/settings/settings.component' | ||||
| import { FormsModule, ReactiveFormsModule } from '@angular/forms' | ||||
| import { DatePipe, registerLocaleData } from '@angular/common' | ||||
| import { NotFoundComponent } from './components/not-found/not-found.component' | ||||
| import { ConfirmDialogComponent } from './components/common/confirm-dialog/confirm-dialog.component' | ||||
| import { CorrespondentEditDialogComponent } from './components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component' | ||||
| import { TagEditDialogComponent } from './components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component' | ||||
| import { DocumentTypeEditDialogComponent } from './components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component' | ||||
| import { TagComponent } from './components/common/tag/tag.component' | ||||
| import { PageHeaderComponent } from './components/common/page-header/page-header.component' | ||||
| import { AppFrameComponent } from './components/app-frame/app-frame.component' | ||||
| import { ToastsComponent } from './components/common/toasts/toasts.component' | ||||
| import { FilterEditorComponent } from './components/document-list/filter-editor/filter-editor.component' | ||||
| import { FilterableDropdownComponent } from './components/common/filterable-dropdown/filterable-dropdown.component' | ||||
| import { ToggleableDropdownButtonComponent } from './components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component' | ||||
| import { DateDropdownComponent } from './components/common/date-dropdown/date-dropdown.component' | ||||
| import { DocumentCardLargeComponent } from './components/document-list/document-card-large/document-card-large.component' | ||||
| import { DocumentCardSmallComponent } from './components/document-list/document-card-small/document-card-small.component' | ||||
| import { BulkEditorComponent } from './components/document-list/bulk-editor/bulk-editor.component' | ||||
| import { NgxFileDropModule } from 'ngx-file-drop' | ||||
| import { TextComponent } from './components/common/input/text/text.component' | ||||
| import { SelectComponent } from './components/common/input/select/select.component' | ||||
| import { CheckComponent } from './components/common/input/check/check.component' | ||||
| import { SaveViewConfigDialogComponent } from './components/document-list/save-view-config-dialog/save-view-config-dialog.component' | ||||
| import { TagsComponent } from './components/common/input/tags/tags.component' | ||||
| import { SortableDirective } from './directives/sortable.directive' | ||||
| import { CookieService } from 'ngx-cookie-service' | ||||
| import { CsrfInterceptor } from './interceptors/csrf.interceptor' | ||||
| import { SavedViewWidgetComponent } from './components/dashboard/widgets/saved-view-widget/saved-view-widget.component' | ||||
| import { StatisticsWidgetComponent } from './components/dashboard/widgets/statistics-widget/statistics-widget.component' | ||||
| import { UploadFileWidgetComponent } from './components/dashboard/widgets/upload-file-widget/upload-file-widget.component' | ||||
| import { WidgetFrameComponent } from './components/dashboard/widgets/widget-frame/widget-frame.component' | ||||
| import { PdfViewerModule } from 'ng2-pdf-viewer' | ||||
| import { WelcomeWidgetComponent } from './components/dashboard/widgets/welcome-widget/welcome-widget.component' | ||||
| import { YesNoPipe } from './pipes/yes-no.pipe' | ||||
| import { FileSizePipe } from './pipes/file-size.pipe' | ||||
| import { FilterPipe } from './pipes/filter.pipe' | ||||
| import { DocumentTitlePipe } from './pipes/document-title.pipe' | ||||
| import { MetadataCollapseComponent } from './components/document-detail/metadata-collapse/metadata-collapse.component' | ||||
| import { SelectDialogComponent } from './components/common/select-dialog/select-dialog.component' | ||||
| import { NgSelectModule } from '@ng-select/ng-select' | ||||
| import { NumberComponent } from './components/common/input/number/number.component' | ||||
| import { SafeUrlPipe } from './pipes/safeurl.pipe' | ||||
| import { SafeHtmlPipe } from './pipes/safehtml.pipe' | ||||
| import { CustomDatePipe } from './pipes/custom-date.pipe' | ||||
| import { DateComponent } from './components/common/input/date/date.component' | ||||
| import { ISODateTimeAdapter } from './utils/ngb-iso-date-time-adapter' | ||||
| import { LocalizedDateParserFormatter } from './utils/ngb-date-parser-formatter' | ||||
| import { ApiVersionInterceptor } from './interceptors/api-version.interceptor' | ||||
| import { ColorSliderModule } from 'ngx-color/slider' | ||||
| import { ColorComponent } from './components/common/input/color/color.component' | ||||
| import { DocumentAsnComponent } from './components/document-asn/document-asn.component' | ||||
|  | ||||
| import localeBe from '@angular/common/locales/be' | ||||
| import localeCs from '@angular/common/locales/cs' | ||||
| import localeDa from '@angular/common/locales/da' | ||||
| import localeDe from '@angular/common/locales/de' | ||||
| import localeEnGb from '@angular/common/locales/en-GB' | ||||
| import localeEs from '@angular/common/locales/es' | ||||
| import localeFr from '@angular/common/locales/fr' | ||||
| import localeIt from '@angular/common/locales/it' | ||||
| import localeLb from '@angular/common/locales/lb' | ||||
| import localeNl from '@angular/common/locales/nl' | ||||
| import localePl from '@angular/common/locales/pl' | ||||
| import localePt from '@angular/common/locales/pt' | ||||
| import localeRo from '@angular/common/locales/ro' | ||||
| import localeRu from '@angular/common/locales/ru' | ||||
| import localeSl from '@angular/common/locales/sl' | ||||
| import localeSr from '@angular/common/locales/sr' | ||||
| import localeSv from '@angular/common/locales/sv' | ||||
| import localeTr from '@angular/common/locales/tr' | ||||
| import localeZh from '@angular/common/locales/zh' | ||||
|  | ||||
| registerLocaleData(localeBe) | ||||
| registerLocaleData(localeCs) | ||||
| registerLocaleData(localeDa) | ||||
| registerLocaleData(localeDe) | ||||
| @@ -91,11 +99,15 @@ registerLocaleData(localeIt) | ||||
| registerLocaleData(localeLb) | ||||
| registerLocaleData(localeNl) | ||||
| registerLocaleData(localePl) | ||||
| registerLocaleData(localePt, "pt-BR") | ||||
| registerLocaleData(localePt, "pt-PT") | ||||
| registerLocaleData(localePt, 'pt-BR') | ||||
| registerLocaleData(localePt, 'pt-PT') | ||||
| registerLocaleData(localeRo) | ||||
| registerLocaleData(localeRu) | ||||
| registerLocaleData(localeSl) | ||||
| registerLocaleData(localeSr) | ||||
| registerLocaleData(localeSv) | ||||
| registerLocaleData(localeTr) | ||||
| registerLocaleData(localeZh) | ||||
|  | ||||
| @NgModule({ | ||||
|   declarations: [ | ||||
| @@ -104,8 +116,8 @@ registerLocaleData(localeSv) | ||||
|     DocumentDetailComponent, | ||||
|     DashboardComponent, | ||||
|     TagListComponent, | ||||
|     CorrespondentListComponent, | ||||
|     DocumentTypeListComponent, | ||||
|     CorrespondentListComponent, | ||||
|     LogsComponent, | ||||
|     SettingsComponent, | ||||
|     NotFoundComponent, | ||||
| @@ -142,11 +154,12 @@ registerLocaleData(localeSv) | ||||
|     MetadataCollapseComponent, | ||||
|     SelectDialogComponent, | ||||
|     NumberComponent, | ||||
|     SafePipe, | ||||
|     SafeUrlPipe, | ||||
|     SafeHtmlPipe, | ||||
|     CustomDatePipe, | ||||
|     DateComponent, | ||||
|     ColorComponent, | ||||
|     DocumentAsnComponent | ||||
|     DocumentAsnComponent, | ||||
|   ], | ||||
|   imports: [ | ||||
|     BrowserModule, | ||||
| @@ -156,27 +169,28 @@ registerLocaleData(localeSv) | ||||
|     FormsModule, | ||||
|     ReactiveFormsModule, | ||||
|     NgxFileDropModule, | ||||
|     InfiniteScrollModule, | ||||
|     PdfViewerModule, | ||||
|     NgSelectModule, | ||||
|     ColorSliderModule | ||||
|     ColorSliderModule, | ||||
|   ], | ||||
|   providers: [ | ||||
|     DatePipe, | ||||
|     CookieService, { | ||||
|     CookieService, | ||||
|     { | ||||
|       provide: HTTP_INTERCEPTORS, | ||||
|       useClass: CsrfInterceptor, | ||||
|       multi: true | ||||
|     },{ | ||||
|       multi: true, | ||||
|     }, | ||||
|     { | ||||
|       provide: HTTP_INTERCEPTORS, | ||||
|       useClass: ApiVersionInterceptor, | ||||
|       multi: true | ||||
|       multi: true, | ||||
|     }, | ||||
|     FilterPipe, | ||||
|     DocumentTitlePipe, | ||||
|     {provide: NgbDateAdapter, useClass: ISODateTimeAdapter}, | ||||
|     {provide: NgbDateParserFormatter, useClass: LocalizedDateParserFormatter} | ||||
|     { provide: NgbDateAdapter, useClass: ISODateTimeAdapter }, | ||||
|     { provide: NgbDateParserFormatter, useClass: LocalizedDateParserFormatter }, | ||||
|   ], | ||||
|   bootstrap: [AppComponent] | ||||
|   bootstrap: [AppComponent], | ||||
| }) | ||||
| export class AppModule { } | ||||
| export class AppModule {} | ||||
|   | ||||
| @@ -12,7 +12,7 @@ | ||||
|   </a> | ||||
|   <div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 ps-md-4 me-sm-auto order-3 order-sm-1"> | ||||
|     <form (ngSubmit)="search()" class="form-inline flex-grow-1"> | ||||
|       <svg width="1em" height="1em"> | ||||
|       <svg width="1em" height="1em" fill="currentColor"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#search"/> | ||||
|       </svg> | ||||
|       <input class="form-control form-control-sm" type="text" placeholder="Search documents" aria-label="Search" | ||||
| @@ -25,7 +25,7 @@ | ||||
|         <span *ngIf="displayName" class="navbar-text small me-2 text-light d-none d-sm-inline"> | ||||
|           {{displayName}} | ||||
|         </span> | ||||
|         <svg width="1.3em" height="1.3em"> | ||||
|         <svg width="1.3em" height="1.3em" fill="currentColor"> | ||||
|           <use xlink:href="assets/bootstrap-icons.svg#person-circle"/> | ||||
|         </svg> | ||||
|       </button> | ||||
| @@ -62,7 +62,7 @@ | ||||
|             </a> | ||||
|           </li> | ||||
|           <li class="nav-item"> | ||||
|             <a class="nav-link" routerLink="documents" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" (click)="closeMenu()"> | ||||
|             <a class="nav-link" routerLink="documents" routerLinkActive="active" (click)="closeMenu()"> | ||||
|               <svg class="sidebaricon" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#files"/> | ||||
|               </svg> <ng-container i18n>Documents</ng-container> | ||||
| @@ -92,7 +92,7 @@ | ||||
|               <svg class="sidebaricon" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#file-text"/> | ||||
|               </svg> {{d.title | documentTitle}} | ||||
|               <span class="close bg-light" (click)="closeDocument(d); $event.preventDefault()"> | ||||
|               <span class="close" (click)="closeDocument(d); $event.preventDefault()"> | ||||
|                 <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-x" viewBox="0 0 16 16"> | ||||
|                   <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|                 </svg> | ||||
| @@ -169,22 +169,47 @@ | ||||
|           </li> | ||||
|           <li class="nav-item"> | ||||
|             <div class="d-flex w-100 flex-wrap"> | ||||
|               <a class="nav-link pe-0 pb-0" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx"> | ||||
|                 <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="sidebaricon bi bi-github" viewBox="0 0 16 16"> | ||||
|                   <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/> | ||||
|               <a class="nav-link pe-2 pb-1" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx"> | ||||
|                 <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="sidebaricon" viewBox="0 0 16 16"> | ||||
|                   <use xlink:href="assets/bootstrap-icons.svg#github" /> | ||||
|                 </svg> <ng-container i18n>GitHub</ng-container> | ||||
|               </a> | ||||
|               <a class="nav-link-additional small text-muted ms-3" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx/discussions/categories/feature-requests" title="Suggest an idea"> | ||||
|                 <svg xmlns="http://www.w3.org/2000/svg" width="1.3em" height="1.3em" fill="currentColor" class="bi bi-lightbulb pe-1" viewBox="0 0 16 16"> | ||||
|                   <path d="M2 6a6 6 0 1 1 10.174 4.31c-.203.196-.359.4-.453.619l-.762 1.769A.5.5 0 0 1 10.5 13a.5.5 0 0 1 0 1 .5.5 0 0 1 0 1l-.224.447a1 1 0 0 1-.894.553H6.618a1 1 0 0 1-.894-.553L5.5 15a.5.5 0 0 1 0-1 .5.5 0 0 1 0-1 .5.5 0 0 1-.46-.302l-.761-1.77a1.964 1.964 0 0 0-.453-.618A5.984 5.984 0 0 1 2 6zm6-5a5 5 0 0 0-3.479 8.592c.263.254.514.564.676.941L5.83 12h4.342l.632-1.467c.162-.377.413-.687.676-.941A5 5 0 0 0 8 1z"/> | ||||
|                 <svg xmlns="http://www.w3.org/2000/svg" width="1.1em" height="1.1em" fill="currentColor" class="me-1" viewBox="0 0 16 16"> | ||||
|                   <use xlink:href="assets/bootstrap-icons.svg#lightbulb" /> | ||||
|                 </svg> | ||||
|                 <ng-container i18n>Suggest an idea</ng-container> | ||||
|               </a> | ||||
|             </div> | ||||
|           </li> | ||||
|           <li class="nav-item mt-2"> | ||||
|             <div class="px-3 py-2 text-muted small"> | ||||
|               {{versionString}} | ||||
|             <div class="px-3 py-2 text-muted small d-flex align-items-center flex-wrap"> | ||||
|               <div class="me-3">{{ versionString }}</div> | ||||
|               <div *ngIf="appRemoteVersion" class="version-check"> | ||||
|                 <ng-template #updateAvailablePopContent> | ||||
|                   <span class="small">Paperless-ngx v{{ appRemoteVersion.version }} <ng-container i18n>is available.</ng-container><br/><ng-container i18n>Click to view.</ng-container></span> | ||||
|                 </ng-template> | ||||
|                 <ng-template #updateCheckingNotEnabledPopContent> | ||||
|                   <span class="small"><ng-container i18n>Checking for updates is disabled.</ng-container><br/><ng-container i18n>Click for more information.</ng-container></span> | ||||
|                 </ng-template> | ||||
|                 <ng-container *ngIf="appRemoteVersion.feature_is_set; else updateCheckNotSet"> | ||||
|                   <a *ngIf="appRemoteVersion.update_available" class="small text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx/releases" | ||||
|                   [ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave" container="body"> | ||||
|                     <svg fill="currentColor" class="me-1" 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> | ||||
|                     <ng-container *ngIf="appRemoteVersion?.update_available" i18n>Update available</ng-container> | ||||
|                   </a> | ||||
|                 </ng-container> | ||||
|                 <ng-template #updateCheckNotSet> | ||||
|                   <a class="small text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://paperless-ngx.readthedocs.io/en/latest/configuration.html#update-checking" | ||||
|                   [ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter:mouseleave" container="body"> | ||||
|                     <svg fill="currentColor" class="me-1" 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> | ||||
|                   </a> | ||||
|                 </ng-template> | ||||
|               </div> | ||||
|             </div> | ||||
|           </li> | ||||
|         </ul> | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| @import "/src/theme"; | ||||
| /* | ||||
|  * Sidebar | ||||
|  */ | ||||
| @@ -35,22 +34,24 @@ | ||||
|  | ||||
| .sidebar .nav-link { | ||||
|   font-weight: 500; | ||||
|   color: #333; | ||||
| } | ||||
|  | ||||
| .sidebar .nav-link .sidebaricon { | ||||
|   margin-right: 4px; | ||||
|   color: #999; | ||||
| } | ||||
|   &:hover, &.active, &:focus { | ||||
|     color: var(--bs-primary); | ||||
|   } | ||||
|  | ||||
| .sidebar .nav-link.active { | ||||
|   color: $primary; | ||||
|   font-weight: bold; | ||||
| } | ||||
|   &:focus-visible { | ||||
|     outline: none; | ||||
|     background-color: var(--bs-body-bg); | ||||
|   } | ||||
|  | ||||
| .sidebar .nav-link.active .sidebaricon, | ||||
| .sidebar .nav-link:hover .sidebaricon { | ||||
|   color: inherit; | ||||
|   &.active { | ||||
|     font-weight: bold; | ||||
|   } | ||||
|  | ||||
|   .sidebaricon { | ||||
|     margin-right: 4px; | ||||
|     color: inherit; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .sidebar-heading { | ||||
| @@ -172,10 +173,29 @@ | ||||
|     } | ||||
|  | ||||
|     &:focus { | ||||
|       background-color: #fff; | ||||
|       color: #212529; | ||||
|       background-color: rgba(0, 0, 0, 0.3); | ||||
|       color: var(--bs-light); | ||||
|       flex-grow: 1; | ||||
|       padding-left: 0.5rem; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| .version-check { | ||||
|   animation: pulse 2s ease-in-out 0s 1; | ||||
| } | ||||
|  | ||||
| @keyframes pulse { | ||||
|   0% { | ||||
|     opacity: 0; | ||||
|   } | ||||
|   25% { | ||||
|     opacity: 100%; | ||||
|   } | ||||
|   75% { | ||||
|     opacity: 0; | ||||
|   } | ||||
|   100% { | ||||
|     opacity: 100%; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,25 +0,0 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { AppFrameComponent } from './app-frame.component'; | ||||
|  | ||||
| describe('AppFrameComponent', () => { | ||||
|   let component: AppFrameComponent; | ||||
|   let fixture: ComponentFixture<AppFrameComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ AppFrameComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(AppFrameComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -1,36 +1,53 @@ | ||||
| import { Component } from '@angular/core'; | ||||
| import { FormControl } from '@angular/forms'; | ||||
| import { ActivatedRoute, Router, Params } from '@angular/router'; | ||||
| import { from, Observable, Subscription, BehaviorSubject } from 'rxjs'; | ||||
| import { debounceTime, distinctUntilChanged, map, switchMap, first } from 'rxjs/operators'; | ||||
| import { PaperlessDocument } from 'src/app/data/paperless-document'; | ||||
| import { OpenDocumentsService } from 'src/app/services/open-documents.service'; | ||||
| import { SavedViewService } from 'src/app/services/rest/saved-view.service'; | ||||
| import { SearchService } from 'src/app/services/rest/search.service'; | ||||
| import { environment } from 'src/environments/environment'; | ||||
| 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 { FILTER_FULLTEXT_QUERY } from 'src/app/data/filter-rule-type'; | ||||
| import { Component } from '@angular/core' | ||||
| import { FormControl } from '@angular/forms' | ||||
| import { ActivatedRoute, Router, Params } from '@angular/router' | ||||
| import { from, Observable, Subscription, BehaviorSubject } from 'rxjs' | ||||
| import { | ||||
|   debounceTime, | ||||
|   distinctUntilChanged, | ||||
|   map, | ||||
|   switchMap, | ||||
|   first, | ||||
| } from 'rxjs/operators' | ||||
| import { PaperlessDocument } from 'src/app/data/paperless-document' | ||||
| import { OpenDocumentsService } from 'src/app/services/open-documents.service' | ||||
| import { SavedViewService } from 'src/app/services/rest/saved-view.service' | ||||
| import { SearchService } from 'src/app/services/rest/search.service' | ||||
| import { environment } from 'src/environments/environment' | ||||
| 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 { FILTER_FULLTEXT_QUERY } from 'src/app/data/filter-rule-type' | ||||
| import { | ||||
|   RemoteVersionService, | ||||
|   AppRemoteVersion, | ||||
| } from 'src/app/services/rest/remote-version.service' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-app-frame', | ||||
|   templateUrl: './app-frame.component.html', | ||||
|   styleUrls: ['./app-frame.component.scss'] | ||||
|   styleUrls: ['./app-frame.component.scss'], | ||||
| }) | ||||
| export class AppFrameComponent { | ||||
|  | ||||
|   constructor ( | ||||
|   constructor( | ||||
|     public router: Router, | ||||
|     private activatedRoute: ActivatedRoute, | ||||
|     private openDocumentsService: OpenDocumentsService, | ||||
|     private searchService: SearchService, | ||||
|     public savedViewService: SavedViewService, | ||||
|     private list: DocumentListViewService, | ||||
|     private meta: Meta | ||||
|     ) { } | ||||
|     private meta: Meta, | ||||
|     private remoteVersionService: RemoteVersionService | ||||
|   ) { | ||||
|     this.remoteVersionService | ||||
|       .checkForUpdates() | ||||
|       .subscribe((appRemoteVersion: AppRemoteVersion) => { | ||||
|         this.appRemoteVersion = appRemoteVersion | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   versionString = `${environment.appTitle} ${environment.version}` | ||||
|   appRemoteVersion | ||||
|  | ||||
|   isMenuCollapsed: boolean = true | ||||
|  | ||||
| @@ -48,14 +65,14 @@ export class AppFrameComponent { | ||||
|     text$.pipe( | ||||
|       debounceTime(200), | ||||
|       distinctUntilChanged(), | ||||
|       map(term => { | ||||
|       map((term) => { | ||||
|         if (term.lastIndexOf(' ') != -1) { | ||||
|           return term.substring(term.lastIndexOf(' ') + 1) | ||||
|         } else { | ||||
|           return term | ||||
|         } | ||||
|       }), | ||||
|       switchMap(term => | ||||
|       switchMap((term) => | ||||
|         term.length < 2 ? from([[]]) : this.searchService.autocomplete(term) | ||||
|       ) | ||||
|     ) | ||||
| @@ -66,49 +83,63 @@ export class AppFrameComponent { | ||||
|     let lastSpaceIndex = currentSearch.lastIndexOf(' ') | ||||
|     if (lastSpaceIndex != -1) { | ||||
|       currentSearch = currentSearch.substring(0, lastSpaceIndex + 1) | ||||
|       currentSearch += event.item + " " | ||||
|       currentSearch += event.item + ' ' | ||||
|     } else { | ||||
|       currentSearch = event.item + " " | ||||
|       currentSearch = event.item + ' ' | ||||
|     } | ||||
|     this.searchField.patchValue(currentSearch) | ||||
|   } | ||||
|  | ||||
|   search() { | ||||
|     this.closeMenu() | ||||
|     this.list.quickFilter([{rule_type: FILTER_FULLTEXT_QUERY, value: this.searchField.value}]) | ||||
|     this.list.quickFilter([ | ||||
|       { | ||||
|         rule_type: FILTER_FULLTEXT_QUERY, | ||||
|         value: (this.searchField.value as string).trim(), | ||||
|       }, | ||||
|     ]) | ||||
|   } | ||||
|  | ||||
|   closeDocument(d: PaperlessDocument) { | ||||
|     this.openDocumentsService.closeDocument(d).pipe(first()).subscribe(confirmed => { | ||||
|       if (confirmed) { | ||||
|         this.closeMenu() | ||||
|         let route = this.activatedRoute.snapshot | ||||
|         while (route.firstChild) { | ||||
|           route = route.firstChild | ||||
|     this.openDocumentsService | ||||
|       .closeDocument(d) | ||||
|       .pipe(first()) | ||||
|       .subscribe((confirmed) => { | ||||
|         if (confirmed) { | ||||
|           this.closeMenu() | ||||
|           let route = this.activatedRoute.snapshot | ||||
|           while (route.firstChild) { | ||||
|             route = route.firstChild | ||||
|           } | ||||
|           if ( | ||||
|             route.component == DocumentDetailComponent && | ||||
|             route.params['id'] == d.id | ||||
|           ) { | ||||
|             this.router.navigate(['']) | ||||
|           } | ||||
|         } | ||||
|         if (route.component == DocumentDetailComponent && route.params['id'] == d.id) { | ||||
|           this.router.navigate([""]) | ||||
|         } | ||||
|       } | ||||
|     }) | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   closeAll() { | ||||
|     // user may need to confirm losing unsaved changes | ||||
|     this.openDocumentsService.closeAll().pipe(first()).subscribe(confirmed => { | ||||
|       if (confirmed) { | ||||
|         this.closeMenu() | ||||
|     this.openDocumentsService | ||||
|       .closeAll() | ||||
|       .pipe(first()) | ||||
|       .subscribe((confirmed) => { | ||||
|         if (confirmed) { | ||||
|           this.closeMenu() | ||||
|  | ||||
|         // TODO: is there a better way to do this? | ||||
|         let route = this.activatedRoute | ||||
|         while (route.firstChild) { | ||||
|           route = route.firstChild | ||||
|           // TODO: is there a better way to do this? | ||||
|           let route = this.activatedRoute | ||||
|           while (route.firstChild) { | ||||
|             route = route.firstChild | ||||
|           } | ||||
|           if (route.component === DocumentDetailComponent) { | ||||
|             this.router.navigate(['']) | ||||
|           } | ||||
|         } | ||||
|         if (route.component === DocumentDetailComponent) { | ||||
|           this.router.navigate([""]) | ||||
|         } | ||||
|       } | ||||
|     }) | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   get displayName() { | ||||
| @@ -123,5 +154,4 @@ export class AppFrameComponent { | ||||
|       return null | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -8,9 +8,12 @@ | ||||
|       <p *ngIf="message">{{message}}</p> | ||||
|     </div> | ||||
|     <div class="modal-footer"> | ||||
|       <button type="button" class="btn btn-outline-dark" (click)="cancel()" [disabled]="!buttonsEnabled" i18n>Cancel</button> | ||||
|       <button type="button" class="btn btn-outline-secondary" (click)="cancel()" [disabled]="!buttonsEnabled" i18n> | ||||
|         <span class="d-inline-block" style="padding-bottom: 1px;" >Cancel</span> | ||||
|       </button> | ||||
|       <button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled"> | ||||
|         {{btnCaption}} | ||||
|         <span *ngIf="!confirmButtonEnabled"> ({{seconds}})</span> | ||||
|         <ngb-progressbar *ngIf="!confirmButtonEnabled" style="height: 1px;" type="dark" [max]="secondsTotal" [value]="seconds"></ngb-progressbar> | ||||
|         <span class="visually-hidden">{{ seconds | number: '1.0-0' }} seconds</span> | ||||
|       </button> | ||||
|     </div> | ||||
|   | ||||
| @@ -1,25 +0,0 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { ConfirmDialogComponent } from './confirm-dialog.component'; | ||||
|  | ||||
| describe('ConfirmDialogComponent', () => { | ||||
|   let component: ConfirmDialogComponent; | ||||
|   let fixture: ComponentFixture<ConfirmDialogComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ ConfirmDialogComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(ConfirmDialogComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -1,15 +1,14 @@ | ||||
| import { Component, EventEmitter, Input, Output } from '@angular/core'; | ||||
| import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; | ||||
| import { Subject } from 'rxjs'; | ||||
| import { Component, EventEmitter, Input, Output } from '@angular/core' | ||||
| import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { interval, Subject, switchMap, take } from 'rxjs' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-confirm-dialog', | ||||
|   templateUrl: './confirm-dialog.component.html', | ||||
|   styleUrls: ['./confirm-dialog.component.scss'] | ||||
|   styleUrls: ['./confirm-dialog.component.scss'], | ||||
| }) | ||||
| export class ConfirmDialogComponent { | ||||
|  | ||||
|   constructor(public activeModal: NgbActiveModal) { } | ||||
|   constructor(public activeModal: NgbActiveModal) {} | ||||
|  | ||||
|   @Output() | ||||
|   public confirmClicked = new EventEmitter() | ||||
| @@ -24,7 +23,7 @@ export class ConfirmDialogComponent { | ||||
|   message | ||||
|  | ||||
|   @Input() | ||||
|   btnClass = "btn-primary" | ||||
|   btnClass = 'btn-primary' | ||||
|  | ||||
|   @Input() | ||||
|   btnCaption = $localize`Confirm` | ||||
| @@ -34,19 +33,28 @@ export class ConfirmDialogComponent { | ||||
|  | ||||
|   confirmButtonEnabled = true | ||||
|   seconds = 0 | ||||
|   secondsTotal = 0 | ||||
|  | ||||
|   confirmSubject: Subject<boolean> | ||||
|  | ||||
|   delayConfirm(seconds: number) { | ||||
|     this.confirmButtonEnabled = false | ||||
|     const refreshInterval = 0.15 // s | ||||
|  | ||||
|     this.secondsTotal = seconds | ||||
|     this.seconds = seconds | ||||
|     setTimeout(() => { | ||||
|       if (this.seconds <= 1) { | ||||
|         this.confirmButtonEnabled = true | ||||
|       } else { | ||||
|         this.delayConfirm(seconds - 1) | ||||
|       } | ||||
|     }, 1000) | ||||
|  | ||||
|     interval(refreshInterval * 1000) | ||||
|       .pipe( | ||||
|         take(this.secondsTotal / refreshInterval + 2) // need 2 more for animation to complete after 0 | ||||
|       ) | ||||
|       .subscribe((count) => { | ||||
|         this.seconds = Math.max( | ||||
|           0, | ||||
|           this.secondsTotal - refreshInterval * (count + 1) | ||||
|         ) | ||||
|         this.confirmButtonEnabled = | ||||
|           this.secondsTotal - refreshInterval * count < 0 | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   cancel() { | ||||
|   | ||||
| @@ -20,8 +20,8 @@ | ||||
|           </div> | ||||
|  | ||||
|           <div class="input-group input-group-sm"> | ||||
|             <input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" | ||||
|                     [(ngModel)]="dateAfter" ngbDatepicker #dateAfterPicker="ngbDatepicker"> | ||||
|             <input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)" | ||||
|                     maxlength="10" [(ngModel)]="dateAfter" ngbDatepicker #dateAfterPicker="ngbDatepicker"> | ||||
|             <button class="btn btn-outline-secondary" (click)="dateAfterPicker.toggle()" type="button"> | ||||
|               <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="bi bi-calendar" viewBox="0 0 16 16"> | ||||
|                 <path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/> | ||||
| @@ -43,8 +43,8 @@ | ||||
|           </div> | ||||
|  | ||||
|           <div class="input-group input-group-sm"> | ||||
|             <input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" | ||||
|                     [(ngModel)]="dateBefore" ngbDatepicker #dateBeforePicker="ngbDatepicker"> | ||||
|             <input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)" | ||||
|                     maxlength="10" [(ngModel)]="dateBefore" ngbDatepicker #dateBeforePicker="ngbDatepicker"> | ||||
|             <button class="btn btn-outline-secondary" (click)="dateBeforePicker.toggle()" type="button"> | ||||
|               <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="bi bi-calendar" viewBox="0 0 16 16"> | ||||
|                 <path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/> | ||||
|   | ||||
| @@ -1,25 +0,0 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { DateDropdownComponent } from './date-dropdown.component'; | ||||
|  | ||||
| describe('DateDropdownComponent', () => { | ||||
|   let component: DateDropdownComponent; | ||||
|   let fixture: ComponentFixture<DateDropdownComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ DateDropdownComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(DateDropdownComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -1,10 +1,17 @@ | ||||
| import { formatDate } from '@angular/common'; | ||||
| import { Component, EventEmitter, Input, Output, OnInit, OnDestroy } from '@angular/core'; | ||||
| import { NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; | ||||
| import { Subject, Subscription } from 'rxjs'; | ||||
| import { debounceTime } from 'rxjs/operators'; | ||||
| import { SettingsService } from 'src/app/services/settings.service'; | ||||
| import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'; | ||||
| import { formatDate } from '@angular/common' | ||||
| import { | ||||
|   Component, | ||||
|   EventEmitter, | ||||
|   Input, | ||||
|   Output, | ||||
|   OnInit, | ||||
|   OnDestroy, | ||||
| } from '@angular/core' | ||||
| import { NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { Subject, Subscription } from 'rxjs' | ||||
| import { debounceTime } from 'rxjs/operators' | ||||
| import { SettingsService } from 'src/app/services/settings.service' | ||||
| import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter' | ||||
|  | ||||
| export interface DateSelection { | ||||
|   before?: string | ||||
| @@ -20,21 +27,18 @@ const LAST_YEAR = 3 | ||||
|   selector: 'app-date-dropdown', | ||||
|   templateUrl: './date-dropdown.component.html', | ||||
|   styleUrls: ['./date-dropdown.component.scss'], | ||||
|   providers: [ | ||||
|     {provide: NgbDateAdapter, useClass: ISODateAdapter}, | ||||
|   ] | ||||
|   providers: [{ provide: NgbDateAdapter, useClass: ISODateAdapter }], | ||||
| }) | ||||
| export class DateDropdownComponent implements OnInit, OnDestroy { | ||||
|  | ||||
|   constructor(settings: SettingsService) { | ||||
|     this.datePlaceHolder = settings.getLocalizedDateInputFormat() | ||||
|   } | ||||
|  | ||||
|   quickFilters = [ | ||||
|     {id: LAST_7_DAYS, name: $localize`Last 7 days`}, | ||||
|     {id: LAST_MONTH, name: $localize`Last month`}, | ||||
|     {id: LAST_3_MONTHS, name: $localize`Last 3 months`}, | ||||
|     {id: LAST_YEAR, name: $localize`Last year`} | ||||
|     { id: LAST_7_DAYS, name: $localize`Last 7 days` }, | ||||
|     { id: LAST_MONTH, name: $localize`Last month` }, | ||||
|     { id: LAST_3_MONTHS, name: $localize`Last 3 months` }, | ||||
|     { id: LAST_YEAR, name: $localize`Last year` }, | ||||
|   ] | ||||
|  | ||||
|   datePlaceHolder: string | ||||
| @@ -62,9 +66,7 @@ export class DateDropdownComponent implements OnInit, OnDestroy { | ||||
|   private sub: Subscription | ||||
|  | ||||
|   ngOnInit() { | ||||
|     this.sub = this.datesSetDebounce$.pipe( | ||||
|       debounceTime(400) | ||||
|     ).subscribe(() => { | ||||
|     this.sub = this.datesSetDebounce$.pipe(debounceTime(400)).subscribe(() => { | ||||
|       this.onChange() | ||||
|     }) | ||||
|   } | ||||
| @@ -81,11 +83,11 @@ export class DateDropdownComponent implements OnInit, OnDestroy { | ||||
|     switch (qf) { | ||||
|       case LAST_7_DAYS: | ||||
|         date.setDate(date.getDate() - 7) | ||||
|         break; | ||||
|         break | ||||
|  | ||||
|       case LAST_MONTH: | ||||
|         date.setMonth(date.getMonth() - 1) | ||||
|         break; | ||||
|         break | ||||
|  | ||||
|       case LAST_3_MONTHS: | ||||
|         date.setMonth(date.getMonth() - 3) | ||||
| @@ -94,20 +96,22 @@ export class DateDropdownComponent implements OnInit, OnDestroy { | ||||
|       case LAST_YEAR: | ||||
|         date.setFullYear(date.getFullYear() - 1) | ||||
|         break | ||||
|  | ||||
|       } | ||||
|     this.dateAfter = formatDate(date, 'yyyy-MM-dd', "en-us", "UTC") | ||||
|     } | ||||
|     this.dateAfter = formatDate(date, 'yyyy-MM-dd', 'en-us', 'UTC') | ||||
|     this.onChange() | ||||
|   } | ||||
|  | ||||
|   onChange() { | ||||
|     this.dateAfterChange.emit(this.dateAfter) | ||||
|     this.dateBeforeChange.emit(this.dateBefore) | ||||
|     this.datesSet.emit({after: this.dateAfter, before: this.dateBefore}) | ||||
|     this.datesSet.emit({ after: this.dateAfter, before: this.dateBefore }) | ||||
|   } | ||||
|  | ||||
|   onChangeDebounce() { | ||||
|     this.datesSetDebounce$.next({after: this.dateAfter, before: this.dateBefore}) | ||||
|     this.datesSetDebounce$.next({ | ||||
|       after: this.dateAfter, | ||||
|       before: this.dateBefore, | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   clearBefore() { | ||||
| @@ -120,4 +124,10 @@ export class DateDropdownComponent implements OnInit, OnDestroy { | ||||
|     this.onChange() | ||||
|   } | ||||
|  | ||||
|   // prevent chars other than numbers and separators | ||||
|   onKeyPress(event: KeyboardEvent) { | ||||
|     if ('Enter' !== event.key && !/[0-9,\.\/-]+/.test(event.key)) { | ||||
|       event.preventDefault() | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -11,7 +11,7 @@ | ||||
|     <app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive" novalidate></app-input-check> | ||||
|   </div> | ||||
|   <div class="modal-footer"> | ||||
|     <button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> | ||||
|     <button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> | ||||
|     <button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button> | ||||
|   </div> | ||||
| </form> | ||||
| @@ -1,19 +1,22 @@ | ||||
| import { Component } from '@angular/core'; | ||||
| import { FormControl, FormGroup } from '@angular/forms'; | ||||
| import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; | ||||
| import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'; | ||||
| import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'; | ||||
| import { CorrespondentService } from 'src/app/services/rest/correspondent.service'; | ||||
| import { ToastService } from 'src/app/services/toast.service'; | ||||
| import { Component } from '@angular/core' | ||||
| import { FormControl, FormGroup } from '@angular/forms' | ||||
| import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' | ||||
| import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent' | ||||
| import { CorrespondentService } from 'src/app/services/rest/correspondent.service' | ||||
| import { ToastService } from 'src/app/services/toast.service' | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-correspondent-edit-dialog', | ||||
|   templateUrl: './correspondent-edit-dialog.component.html', | ||||
|   styleUrls: ['./correspondent-edit-dialog.component.scss'] | ||||
|   styleUrls: ['./correspondent-edit-dialog.component.scss'], | ||||
| }) | ||||
| export class CorrespondentEditDialogComponent extends EditDialogComponent<PaperlessCorrespondent> { | ||||
| 
 | ||||
|   constructor(service: CorrespondentService, activeModal: NgbActiveModal, toastService: ToastService) { | ||||
|   constructor( | ||||
|     service: CorrespondentService, | ||||
|     activeModal: NgbActiveModal, | ||||
|     toastService: ToastService | ||||
|   ) { | ||||
|     super(service, activeModal, toastService) | ||||
|   } | ||||
| 
 | ||||
| @@ -29,9 +32,8 @@ export class CorrespondentEditDialogComponent extends EditDialogComponent<Paperl | ||||
|     return new FormGroup({ | ||||
|       name: new FormControl(''), | ||||
|       matching_algorithm: new FormControl(1), | ||||
|       match: new FormControl(""), | ||||
|       is_insensitive: new FormControl(true) | ||||
|       match: new FormControl(''), | ||||
|       is_insensitive: new FormControl(true), | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| @@ -13,7 +13,7 @@ | ||||
| 
 | ||||
|     </div> | ||||
|     <div class="modal-footer"> | ||||
|       <button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> | ||||
|       <button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> | ||||
|       <button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button> | ||||
|     </div> | ||||
|   </form> | ||||
| @@ -1,19 +1,22 @@ | ||||
| import { Component } from '@angular/core'; | ||||
| import { FormControl, FormGroup } from '@angular/forms'; | ||||
| import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; | ||||
| import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'; | ||||
| import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'; | ||||
| import { DocumentTypeService } from 'src/app/services/rest/document-type.service'; | ||||
| import { ToastService } from 'src/app/services/toast.service'; | ||||
| import { Component } from '@angular/core' | ||||
| import { FormControl, FormGroup } from '@angular/forms' | ||||
| import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' | ||||
| import { PaperlessDocumentType } from 'src/app/data/paperless-document-type' | ||||
| import { DocumentTypeService } from 'src/app/services/rest/document-type.service' | ||||
| import { ToastService } from 'src/app/services/toast.service' | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-document-type-edit-dialog', | ||||
|   templateUrl: './document-type-edit-dialog.component.html', | ||||
|   styleUrls: ['./document-type-edit-dialog.component.scss'] | ||||
|   styleUrls: ['./document-type-edit-dialog.component.scss'], | ||||
| }) | ||||
| export class DocumentTypeEditDialogComponent extends EditDialogComponent<PaperlessDocumentType> { | ||||
| 
 | ||||
|   constructor(service: DocumentTypeService, activeModal: NgbActiveModal, toastService: ToastService) { | ||||
|   constructor( | ||||
|     service: DocumentTypeService, | ||||
|     activeModal: NgbActiveModal, | ||||
|     toastService: ToastService | ||||
|   ) { | ||||
|     super(service, activeModal, toastService) | ||||
|   } | ||||
| 
 | ||||
| @@ -29,9 +32,8 @@ export class DocumentTypeEditDialogComponent extends EditDialogComponent<Paperle | ||||
|     return new FormGroup({ | ||||
|       name: new FormControl(''), | ||||
|       matching_algorithm: new FormControl(1), | ||||
|       match: new FormControl(""), | ||||
|       is_insensitive: new FormControl(true) | ||||
|       match: new FormControl(''), | ||||
|       is_insensitive: new FormControl(true), | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| @@ -1,25 +0,0 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { EditDialogComponent } from './edit-dialog.component'; | ||||
|  | ||||
| describe('EditDialogComponent', () => { | ||||
|   let component: EditDialogComponent; | ||||
|   let fixture: ComponentFixture<EditDialogComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ EditDialogComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(EditDialogComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -1,20 +1,22 @@ | ||||
| import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core'; | ||||
| import { FormGroup } from '@angular/forms'; | ||||
| import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; | ||||
| import { Observable } from 'rxjs'; | ||||
| import { map } from 'rxjs/operators'; | ||||
| import { MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model'; | ||||
| import { ObjectWithId } from 'src/app/data/object-with-id'; | ||||
| import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'; | ||||
| import { ToastService } from 'src/app/services/toast.service'; | ||||
| import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core' | ||||
| import { FormGroup } from '@angular/forms' | ||||
| import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { Observable } from 'rxjs' | ||||
| import { map } from 'rxjs/operators' | ||||
| import { MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model' | ||||
| import { ObjectWithId } from 'src/app/data/object-with-id' | ||||
| import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service' | ||||
| import { ToastService } from 'src/app/services/toast.service' | ||||
|  | ||||
| @Directive() | ||||
| export abstract class EditDialogComponent<T extends ObjectWithId> implements OnInit { | ||||
|  | ||||
| export abstract class EditDialogComponent<T extends ObjectWithId> | ||||
|   implements OnInit | ||||
| { | ||||
|   constructor( | ||||
|     private service: AbstractPaperlessService<T>, | ||||
|     private activeModal: NgbActiveModal, | ||||
|     private toastService: ToastService) { } | ||||
|     private toastService: ToastService | ||||
|   ) {} | ||||
|  | ||||
|   @Input() | ||||
|   dialogMode: string = 'create' | ||||
| @@ -43,7 +45,7 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI | ||||
|     // wait to enable close button so it doesnt steal focus from input since its the first clickable element in the DOM | ||||
|     setTimeout(() => { | ||||
|       this.closeEnabled = true | ||||
|     }); | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   getCreateTitle() { | ||||
| @@ -65,7 +67,7 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI | ||||
|       case 'edit': | ||||
|         return this.getEditTitle() | ||||
|       default: | ||||
|         break; | ||||
|         break | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -78,25 +80,31 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI | ||||
|   } | ||||
|  | ||||
|   save() { | ||||
|     var newObject = Object.assign(Object.assign({}, this.object), this.objectForm.value) | ||||
|     var newObject = Object.assign( | ||||
|       Object.assign({}, this.object), | ||||
|       this.objectForm.value | ||||
|     ) | ||||
|     var serverResponse: Observable<T> | ||||
|     switch (this.dialogMode) { | ||||
|       case 'create': | ||||
|         serverResponse = this.service.create(newObject) | ||||
|         break; | ||||
|         break | ||||
|       case 'edit': | ||||
|         serverResponse = this.service.update(newObject) | ||||
|       default: | ||||
|         break; | ||||
|         break | ||||
|     } | ||||
|     this.networkActive = true | ||||
|     serverResponse.subscribe(result => { | ||||
|       this.activeModal.close() | ||||
|       this.success.emit(result) | ||||
|     }, error => { | ||||
|       this.error = error.error | ||||
|       this.networkActive = false | ||||
|     }) | ||||
|     serverResponse.subscribe( | ||||
|       (result) => { | ||||
|         this.activeModal.close() | ||||
|         this.success.emit(result) | ||||
|       }, | ||||
|       (error) => { | ||||
|         this.error = error.error | ||||
|         this.networkActive = false | ||||
|       } | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   cancel() { | ||||
|   | ||||
| @@ -15,7 +15,7 @@ | ||||
|       <app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check> | ||||
|     </div> | ||||
|     <div class="modal-footer"> | ||||
|       <button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> | ||||
|       <button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> | ||||
|       <button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button> | ||||
|     </div> | ||||
|   </form> | ||||
| @@ -0,0 +1,42 @@ | ||||
| import { Component } from '@angular/core' | ||||
| import { FormControl, FormGroup } from '@angular/forms' | ||||
| import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' | ||||
| import { PaperlessTag } from 'src/app/data/paperless-tag' | ||||
| import { TagService } from 'src/app/services/rest/tag.service' | ||||
| import { ToastService } from 'src/app/services/toast.service' | ||||
| import { randomColor } from 'src/app/utils/color' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-tag-edit-dialog', | ||||
|   templateUrl: './tag-edit-dialog.component.html', | ||||
|   styleUrls: ['./tag-edit-dialog.component.scss'], | ||||
| }) | ||||
| export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> { | ||||
|   constructor( | ||||
|     service: TagService, | ||||
|     activeModal: NgbActiveModal, | ||||
|     toastService: ToastService | ||||
|   ) { | ||||
|     super(service, activeModal, toastService) | ||||
|   } | ||||
|  | ||||
|   getCreateTitle() { | ||||
|     return $localize`Create new tag` | ||||
|   } | ||||
|  | ||||
|   getEditTitle() { | ||||
|     return $localize`Edit tag` | ||||
|   } | ||||
|  | ||||
|   getForm(): FormGroup { | ||||
|     return new FormGroup({ | ||||
|       name: new FormControl(''), | ||||
|       color: new FormControl(randomColor()), | ||||
|       is_inbox_tag: new FormControl(false), | ||||
|       matching_algorithm: new FormControl(1), | ||||
|       match: new FormControl(''), | ||||
|       is_insensitive: new FormControl(true), | ||||
|     }) | ||||
|   } | ||||
| } | ||||
| @@ -6,7 +6,7 @@ | ||||
|     <div class="d-none d-sm-inline"> {{title}}</div> | ||||
|     <ng-container *ngIf="!editing && selectionModel.selectionSize() > 0"> | ||||
|       <div *ngIf="multiple" class="position-absolute top-0 start-100 translate-middle badge bg-secondary border border-light text-light rounded-pill"> | ||||
|         {{selectionModel.selectionSize()}}<span class="visually-hidden">selected</span> | ||||
|         {{selectionModel.totalCount}}<span class="visually-hidden">selected</span> | ||||
|       </div> | ||||
|       <div *ngIf="!multiple" class="position-absolute top-0 start-100 p-2 translate-middle badge bg-secondary border border-light rounded-circle"> | ||||
|         <span class="visually-hidden">selected</span> | ||||
| @@ -18,10 +18,10 @@ | ||||
|       <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"> | ||||
|           <label ngbButtonLabel class="btn btn-outline-primary"> | ||||
|             <input ngbButton type="radio" class="btn-check" name="logicalOperator" value="and"> All | ||||
|             <input ngbButton type="radio" class="btn-check" name="logicalOperator" value="and" i18n> All | ||||
|           </label> | ||||
|           <label ngbButtonLabel class="btn btn-outline-primary"> | ||||
|             <input ngbButton type="radio" class="btn-check" name="logicalOperator" value="or"> Any | ||||
|             <input ngbButton type="radio" class="btn-check" name="logicalOperator" value="or" i18n> Any | ||||
|           </label> | ||||
|         </div> | ||||
|       </div> | ||||
|   | ||||
| @@ -1,5 +1,3 @@ | ||||
| @import "/src/theme"; | ||||
|  | ||||
| .badge-corner { | ||||
|   position: absolute; | ||||
|   top: -8px; | ||||
| @@ -42,7 +40,7 @@ | ||||
|   filter: brightness(0.5); | ||||
|  | ||||
|   &.active { | ||||
|     background-color: lighten($primary, 30%); | ||||
|     background-color: var(--pngx-primary-lighten-30); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -60,4 +58,4 @@ small > svg { | ||||
|  | ||||
| .show .btn-outline-primary { | ||||
|   color: #fff; | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,25 +0,0 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { FilterableDropodownComponent } from './filterable-dropdown.component'; | ||||
|  | ||||
| describe('FilterableDropodownComponent', () => { | ||||
|   let component: FilterableDropodownComponent; | ||||
|   let fixture: ComponentFixture<FilterableDropodownComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ FilterableDropodownComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(FilterableDropodownComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -1,17 +1,23 @@ | ||||
| import { Component, EventEmitter, Input, Output, ElementRef, ViewChild } from '@angular/core'; | ||||
| import { FilterPipe } from  'src/app/pipes/filter.pipe'; | ||||
| import { | ||||
|   Component, | ||||
|   EventEmitter, | ||||
|   Input, | ||||
|   Output, | ||||
|   ElementRef, | ||||
|   ViewChild, | ||||
| } from '@angular/core' | ||||
| import { FilterPipe } from 'src/app/pipes/filter.pipe' | ||||
| import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { ToggleableItemState } from './toggleable-dropdown-button/toggleable-dropdown-button.component'; | ||||
| import { MatchingModel } from 'src/app/data/matching-model'; | ||||
| import { Subject } from 'rxjs'; | ||||
| import { ToggleableItemState } from './toggleable-dropdown-button/toggleable-dropdown-button.component' | ||||
| import { MatchingModel } from 'src/app/data/matching-model' | ||||
| import { Subject } from 'rxjs' | ||||
|  | ||||
| export interface ChangedItems { | ||||
|   itemsToAdd: MatchingModel[], | ||||
|   itemsToAdd: MatchingModel[] | ||||
|   itemsToRemove: MatchingModel[] | ||||
| } | ||||
|  | ||||
| export class FilterableDropdownSelectionModel { | ||||
|  | ||||
|   changed = new Subject<FilterableDropdownSelectionModel>() | ||||
|  | ||||
|   multiple = false | ||||
| @@ -22,14 +28,20 @@ export class FilterableDropdownSelectionModel { | ||||
|  | ||||
|   get itemsSorted(): MatchingModel[] { | ||||
|     // TODO: this is getting called very often | ||||
|     return this.items.sort((a,b) => { | ||||
|     return this.items.sort((a, b) => { | ||||
|       if (a.id == null && b.id != null) { | ||||
|         return -1 | ||||
|       } else if (a.id != null && b.id == null) { | ||||
|         return 1 | ||||
|       } else if (this.getNonTemporary(a.id) == ToggleableItemState.NotSelected && this.getNonTemporary(b.id) != ToggleableItemState.NotSelected) { | ||||
|       } else if ( | ||||
|         this.getNonTemporary(a.id) == ToggleableItemState.NotSelected && | ||||
|         this.getNonTemporary(b.id) != ToggleableItemState.NotSelected | ||||
|       ) { | ||||
|         return 1 | ||||
|       } else if (this.getNonTemporary(a.id) != ToggleableItemState.NotSelected && this.getNonTemporary(b.id) == ToggleableItemState.NotSelected) { | ||||
|       } else if ( | ||||
|         this.getNonTemporary(a.id) != ToggleableItemState.NotSelected && | ||||
|         this.getNonTemporary(b.id) == ToggleableItemState.NotSelected | ||||
|       ) { | ||||
|         return -1 | ||||
|       } else { | ||||
|         return a.name.localeCompare(b.name) | ||||
| @@ -42,11 +54,17 @@ export class FilterableDropdownSelectionModel { | ||||
|   private temporarySelectionStates = new Map<number, ToggleableItemState>() | ||||
|  | ||||
|   getSelectedItems() { | ||||
|     return this.items.filter(i => this.temporarySelectionStates.get(i.id) == ToggleableItemState.Selected) | ||||
|     return this.items.filter( | ||||
|       (i) => | ||||
|         this.temporarySelectionStates.get(i.id) == ToggleableItemState.Selected | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   getExcludedItems() { | ||||
|     return this.items.filter(i => this.temporarySelectionStates.get(i.id) == ToggleableItemState.Excluded) | ||||
|     return this.items.filter( | ||||
|       (i) => | ||||
|         this.temporarySelectionStates.get(i.id) == ToggleableItemState.Excluded | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   set(id: number, state: ToggleableItemState, fireEvent = true) { | ||||
| @@ -62,9 +80,16 @@ export class FilterableDropdownSelectionModel { | ||||
|  | ||||
|   toggle(id: number, fireEvent = true) { | ||||
|     let state = this.temporarySelectionStates.get(id) | ||||
|     if (state == null || (state != ToggleableItemState.Selected && state != ToggleableItemState.Excluded)) { | ||||
|     if ( | ||||
|       state == null || | ||||
|       (state != ToggleableItemState.Selected && | ||||
|         state != ToggleableItemState.Excluded) | ||||
|     ) { | ||||
|       this.temporarySelectionStates.set(id, ToggleableItemState.Selected) | ||||
|     } else if (state == ToggleableItemState.Selected || state == ToggleableItemState.Excluded) { | ||||
|     } else if ( | ||||
|       state == ToggleableItemState.Selected || | ||||
|       state == ToggleableItemState.Excluded | ||||
|     ) { | ||||
|       this.temporarySelectionStates.delete(id) | ||||
|     } | ||||
|  | ||||
| @@ -91,7 +116,7 @@ export class FilterableDropdownSelectionModel { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   exclude(id: number, fireEvent:boolean = true) { | ||||
|   exclude(id: number, fireEvent: boolean = true) { | ||||
|     let state = this.temporarySelectionStates.get(id) | ||||
|     if (state == null || state != ToggleableItemState.Excluded) { | ||||
|       this.temporarySelectionStates.set(id, ToggleableItemState.Excluded) | ||||
| @@ -130,13 +155,19 @@ export class FilterableDropdownSelectionModel { | ||||
|   } | ||||
|  | ||||
|   get(id: number) { | ||||
|     return this.temporarySelectionStates.get(id) || ToggleableItemState.NotSelected | ||||
|     return ( | ||||
|       this.temporarySelectionStates.get(id) || ToggleableItemState.NotSelected | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   selectionSize() { | ||||
|     return this.getSelectedItems().length | ||||
|   } | ||||
|  | ||||
|   get totalCount() { | ||||
|     return this.getSelectedItems().length + this.getExcludedItems().length | ||||
|   } | ||||
|  | ||||
|   clear(fireEvent = true) { | ||||
|     this.temporarySelectionStates.clear() | ||||
|     this.temporaryLogicalOperator = this._logicalOperator = 'and' | ||||
| @@ -146,9 +177,19 @@ export class FilterableDropdownSelectionModel { | ||||
|   } | ||||
|  | ||||
|   isDirty() { | ||||
|     if (!Array.from(this.temporarySelectionStates.keys()).every(id => this.temporarySelectionStates.get(id) == this.selectionStates.get(id))) { | ||||
|     if ( | ||||
|       !Array.from(this.temporarySelectionStates.keys()).every( | ||||
|         (id) => | ||||
|           this.temporarySelectionStates.get(id) == this.selectionStates.get(id) | ||||
|       ) | ||||
|     ) { | ||||
|       return true | ||||
|     } else if (!Array.from(this.selectionStates.keys()).every(id => this.selectionStates.get(id) == this.temporarySelectionStates.get(id))) { | ||||
|     } else if ( | ||||
|       !Array.from(this.selectionStates.keys()).every( | ||||
|         (id) => | ||||
|           this.selectionStates.get(id) == this.temporarySelectionStates.get(id) | ||||
|       ) | ||||
|     ) { | ||||
|       return true | ||||
|     } else if (this.temporaryLogicalOperator !== this._logicalOperator) { | ||||
|       return true | ||||
| @@ -158,7 +199,10 @@ export class FilterableDropdownSelectionModel { | ||||
|   } | ||||
|  | ||||
|   isNoneSelected() { | ||||
|     return this.selectionSize() == 1 && this.get(null) == ToggleableItemState.Selected | ||||
|     return ( | ||||
|       this.selectionSize() == 1 && | ||||
|       this.get(null) == ToggleableItemState.Selected | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   init(map) { | ||||
| @@ -183,8 +227,17 @@ export class FilterableDropdownSelectionModel { | ||||
|  | ||||
|   diff(): ChangedItems { | ||||
|     return { | ||||
|       itemsToAdd: this.items.filter(item => this.temporarySelectionStates.get(item.id) == ToggleableItemState.Selected && this.selectionStates.get(item.id) != ToggleableItemState.Selected), | ||||
|       itemsToRemove: this.items.filter(item => !this.temporarySelectionStates.has(item.id) && this.selectionStates.has(item.id)), | ||||
|       itemsToAdd: this.items.filter( | ||||
|         (item) => | ||||
|           this.temporarySelectionStates.get(item.id) == | ||||
|             ToggleableItemState.Selected && | ||||
|           this.selectionStates.get(item.id) != ToggleableItemState.Selected | ||||
|       ), | ||||
|       itemsToRemove: this.items.filter( | ||||
|         (item) => | ||||
|           !this.temporarySelectionStates.has(item.id) && | ||||
|           this.selectionStates.has(item.id) | ||||
|       ), | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -192,10 +245,9 @@ export class FilterableDropdownSelectionModel { | ||||
| @Component({ | ||||
|   selector: 'app-filterable-dropdown', | ||||
|   templateUrl: './filterable-dropdown.component.html', | ||||
|   styleUrls: ['./filterable-dropdown.component.scss'] | ||||
|   styleUrls: ['./filterable-dropdown.component.scss'], | ||||
| }) | ||||
| export class FilterableDropdownComponent { | ||||
|  | ||||
|   @ViewChild('listFilterTextInput') listFilterTextInput: ElementRef | ||||
|   @ViewChild('dropdown') dropdown: NgbDropdown | ||||
|  | ||||
| @@ -207,7 +259,7 @@ export class FilterableDropdownComponent { | ||||
|       this._selectionModel.items = Array.from(items) | ||||
|       this._selectionModel.items.unshift({ | ||||
|         name: $localize`:Filter drop down element to filter for documents with no correspondent/type/tag assigned:Not assigned`, | ||||
|         id: null | ||||
|         id: null, | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| @@ -225,7 +277,7 @@ export class FilterableDropdownComponent { | ||||
|       model.items = this.selectionModel.items | ||||
|       model.multiple = this.selectionModel.multiple | ||||
|     } | ||||
|     model.changed.subscribe(updatedModel => { | ||||
|     model.changed.subscribe((updatedModel) => { | ||||
|       this.selectionModelChange.next(updatedModel) | ||||
|     }) | ||||
|     this._selectionModel = model | ||||
| @@ -251,7 +303,7 @@ export class FilterableDropdownComponent { | ||||
|   title: string | ||||
|  | ||||
|   @Input() | ||||
|   filterPlaceholder: string = "" | ||||
|   filterPlaceholder: string = '' | ||||
|  | ||||
|   @Input() | ||||
|   icon: string | ||||
| @@ -272,14 +324,17 @@ export class FilterableDropdownComponent { | ||||
|   open = new EventEmitter() | ||||
|  | ||||
|   get operatorToggleEnabled(): boolean { | ||||
|     return this.selectionModel.selectionSize() > 1 && this.selectionModel.getExcludedItems().length == 0 | ||||
|     return ( | ||||
|       this.selectionModel.selectionSize() > 1 && | ||||
|       this.selectionModel.getExcludedItems().length == 0 | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   modelIsDirty: boolean = false | ||||
|  | ||||
|   constructor(private filterPipe: FilterPipe) { | ||||
|     this.selectionModel = new FilterableDropdownSelectionModel() | ||||
|     this.selectionModelChange.subscribe(updatedModel => { | ||||
|     this.selectionModelChange.subscribe((updatedModel) => { | ||||
|       this.modelIsDirty = updatedModel.isDirty() | ||||
|     }) | ||||
|   } | ||||
| @@ -296,12 +351,12 @@ export class FilterableDropdownComponent { | ||||
|   dropdownOpenChange(open: boolean): void { | ||||
|     if (open) { | ||||
|       setTimeout(() => { | ||||
|         this.listFilterTextInput.nativeElement.focus(); | ||||
|         this.listFilterTextInput.nativeElement.focus() | ||||
|       }, 0) | ||||
|       if (this.editing) { | ||||
|         this.selectionModel.reset() | ||||
|       } | ||||
|       this.open.next() | ||||
|       this.open.next(this) | ||||
|     } else { | ||||
|       this.filterText = '' | ||||
|       if (this.applyOnClose && this.selectionModel.isDirty()) { | ||||
|   | ||||
| @@ -1,25 +0,0 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { ToggleableDropdownButtonComponent } from './toggleable-dropdown-button.component'; | ||||
|  | ||||
| describe('ToggleableDropdownButtonComponent', () => { | ||||
|   let component: ToggleableDropdownButtonComponent; | ||||
|   let fixture: ComponentFixture<ToggleableDropdownButtonComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ ToggleableDropdownButtonComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(ToggleableDropdownButtonComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -1,20 +1,19 @@ | ||||
| import { Component, EventEmitter, Input, Output, OnInit } from '@angular/core'; | ||||
| import { MatchingModel } from 'src/app/data/matching-model'; | ||||
| import { Component, EventEmitter, Input, Output, OnInit } from '@angular/core' | ||||
| import { MatchingModel } from 'src/app/data/matching-model' | ||||
|  | ||||
| export enum ToggleableItemState { | ||||
|   NotSelected = 0, | ||||
|   Selected = 1, | ||||
|   PartiallySelected = 2, | ||||
|   Excluded = 3 | ||||
|   Excluded = 3, | ||||
| } | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-toggleable-dropdown-button', | ||||
|   templateUrl: './toggleable-dropdown-button.component.html', | ||||
|   styleUrls: ['./toggleable-dropdown-button.component.scss'] | ||||
|   styleUrls: ['./toggleable-dropdown-button.component.scss'], | ||||
| }) | ||||
| export class ToggleableDropdownButtonComponent { | ||||
|  | ||||
|   @Input() | ||||
|   item: MatchingModel | ||||
|  | ||||
|   | ||||
| @@ -1,30 +1,29 @@ | ||||
| import { Directive, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; | ||||
| import { ControlValueAccessor } from '@angular/forms'; | ||||
| import { v4 as uuidv4 } from 'uuid'; | ||||
| import { Directive, ElementRef, Input, OnInit, ViewChild } from '@angular/core' | ||||
| import { ControlValueAccessor } from '@angular/forms' | ||||
| import { v4 as uuidv4 } from 'uuid' | ||||
|  | ||||
| @Directive() | ||||
| export class AbstractInputComponent<T> implements OnInit, ControlValueAccessor { | ||||
|  | ||||
|   @ViewChild("inputField") | ||||
|   @ViewChild('inputField') | ||||
|   inputField: ElementRef | ||||
|  | ||||
|   constructor() { } | ||||
|   constructor() {} | ||||
|  | ||||
|   onChange = (newValue: T) => {}; | ||||
|   onChange = (newValue: T) => {} | ||||
|  | ||||
|   onTouched = () => {}; | ||||
|   onTouched = () => {} | ||||
|  | ||||
|   writeValue(newValue: any): void { | ||||
|     this.value = newValue | ||||
|   } | ||||
|   registerOnChange(fn: any): void { | ||||
|     this.onChange = fn; | ||||
|     this.onChange = fn | ||||
|   } | ||||
|   registerOnTouched(fn: any): void { | ||||
|     this.onTouched = fn; | ||||
|     this.onTouched = fn | ||||
|   } | ||||
|   setDisabledState?(isDisabled: boolean): void { | ||||
|     this.disabled = isDisabled; | ||||
|     this.disabled = isDisabled | ||||
|   } | ||||
|  | ||||
|   focus() { | ||||
| @@ -37,7 +36,7 @@ export class AbstractInputComponent<T> implements OnInit, ControlValueAccessor { | ||||
|   title: string | ||||
|  | ||||
|   @Input() | ||||
|   disabled = false; | ||||
|   disabled = false | ||||
|  | ||||
|   @Input() | ||||
|   error: string | ||||
|   | ||||
| @@ -1,25 +0,0 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { CheckComponent } from './check.component'; | ||||
|  | ||||
| describe('CheckComponent', () => { | ||||
|   let component: CheckComponent; | ||||
|   let fixture: ComponentFixture<CheckComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ CheckComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(CheckComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -1,22 +1,22 @@ | ||||
| import { Component, forwardRef, Input, OnInit } from '@angular/core'; | ||||
| import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; | ||||
| import { v4 as uuidv4 } from 'uuid'; | ||||
| import { AbstractInputComponent } from '../abstract-input'; | ||||
| import { Component, forwardRef, Input, OnInit } from '@angular/core' | ||||
| import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | ||||
| import { v4 as uuidv4 } from 'uuid' | ||||
| import { AbstractInputComponent } from '../abstract-input' | ||||
|  | ||||
| @Component({ | ||||
|   providers: [{ | ||||
|     provide: NG_VALUE_ACCESSOR, | ||||
|     useExisting: forwardRef(() => CheckComponent), | ||||
|     multi: true | ||||
|   }], | ||||
|   providers: [ | ||||
|     { | ||||
|       provide: NG_VALUE_ACCESSOR, | ||||
|       useExisting: forwardRef(() => CheckComponent), | ||||
|       multi: true, | ||||
|     }, | ||||
|   ], | ||||
|   selector: 'app-input-check', | ||||
|   templateUrl: './check.component.html', | ||||
|   styleUrls: ['./check.component.scss'] | ||||
|   styleUrls: ['./check.component.scss'], | ||||
| }) | ||||
| export class CheckComponent extends AbstractInputComponent<boolean> { | ||||
|  | ||||
|   constructor() { | ||||
|     super() | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| <div class="mb-3"> | ||||
|   <label [for]="inputId">{{title}}</label> | ||||
|   <label *ngIf="title" [for]="inputId">{{title}}</label> | ||||
|  | ||||
|   <div class="input-group" [class.is-invalid]="error"> | ||||
|     <span class="input-group-text" [style.background-color]="value">   </span> | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon