mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-10-15 04:27:14 +00:00
Merge pull request #695 from paperless-ngx/beta
[Beta] Paperless-ngx v1.7.0 Release Candidate 1
This commit is contained in:
431 files changed
+41946
-13704
No files matched your search
@@ -17,3 +17,5 @@
|
||||
**/htmlcov
|
||||
/src/.pytest_cache
|
||||
.idea
|
||||
.venv/
|
||||
.vscode/
|
||||
+7
-1
@@ -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
|
||||
@@ -1,2 +1,2 @@
|
||||
COMPOSE_PROJECT_NAME=paperless
|
||||
export PROMPT="(pipenv-projectname)$P$G"
|
||||
export PROMPT="(pipenv-projectname)$P$G"
|
||||
+14
-1
@@ -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"
|
||||
+141
-111
@@ -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
|
||||
@@ -61,6 +61,9 @@ target/
|
||||
# PyCharm
|
||||
.idea
|
||||
|
||||
# VS Code
|
||||
.vscode
|
||||
|
||||
# Other stuff that doesn't belong
|
||||
.virtualenv
|
||||
virtualenv
|
||||
|
||||
@@ -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
@@ -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
|
||||
+11
-11
@@ -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
|
||||
|
||||
+28
-26
@@ -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
|
||||
|
||||
|
||||
+38
-102
@@ -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"]
|
||||
@@ -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 = "*"
|
||||
Generated
+626
-527
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
|
||||
@@ -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``
|
||||
|
||||
|
||||
+93
-1
@@ -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/
|
||||
|
||||
+91
-5
@@ -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.
|
||||
+31
-2
@@ -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
|
||||
|
||||
+5
-6
@@ -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?*
|
||||
|
||||
|
||||
+37
-38
@@ -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!
|
||||
|
||||
+9
-5
@@ -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.
|
||||
|
||||
+51
-22
@@ -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
|
||||
|
||||
|
||||
+41
-37
@@ -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
|
||||
|
||||
@@ -45,3 +45,7 @@ testem.log
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Cypress
|
||||
cypress/videos/**/*
|
||||
cypress/screenshots/**/*
|
||||
+29
-8
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"}]}
|
||||
@@ -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}]}
|
||||
@@ -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"}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"correspondents":[],"tags":[3],"document_types":[1]}
|
||||
@@ -0,0 +1 @@
|
||||
{"count":3,"next":null,"previous":null,"results":[{"id":1,"correspondent":9,"document_type":1,"title":"lorem-ipsum","content":"Test document PDF \n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla est purus, ultrices in porttitor \nin, accumsan non quam. Nam consectetur porttitor rhoncus. Curabitur eu est et leo feugiat \nauctor vel quis lorem. Ut et ligula dolor, sit amet consequat lorem. Aliquam porta eros sed \nvelit imperdiet egestas. Maecenas tempus eros ut diam ullamcorper id dictum libero \ntempor. Donec quis augue quis magna condimentum lobortis. Quisque imperdiet ipsum vel \nmagna viverra rutrum. Cras viverra molestie urna, vitae vestibulum turpis varius id. \nVestibulum mollis, arcu iaculis bibendum varius, velit sapien blandit metus, ac posuere lorem \nnulla ac dolor. Maecenas urna elit, tincidunt in dapibus nec, vehicula eu dui. Duis lacinia \nfringilla massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur \nridiculus mus. Ut consequat ultricies est, non rhoncus mauris congue porta. Vivamus viverra \nsuscipit felis eget condimentum. Cum sociis natoque penatibus et magnis dis parturient \nmontes, nascetur ridiculus mus. Integer bibendum sagittis ligula, non faucibus nulla volutpat \nvitae. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. \nIn aliquet quam et velit bibendum accumsan. Cum sociis natoque penatibus et magnis dis \nparturient montes, nascetur ridiculus mus. Vestibulum vitae ipsum nec arcu semper \nadipiscing at ac lacus. Praesent id pellentesque orci. Morbi congue viverra nisl nec rhoncus. \nInteger mattis, ipsum a tincidunt commodo, lacus arcu elementum elit, at mollis eros ante ac \nrisus. In volutpat, ante at pretium ultricies, velit magna suscipit enim, aliquet blandit massa \norci nec lorem. Nulla facilisi. Duis eu vehicula arcu. Nulla facilisi. Maecenas pellentesque \nvolutpat felis, quis tristique ligula luctus vel. Sed nec mi eros. Integer augue enim, sollicitudin \nullamcorper mattis eget, aliquam in est. Morbi sollicitudin libero nec augue dignissim ut \nconsectetur dui volutpat. Nulla facilisi. Mauris egestas vestibulum neque cursus tincidunt. \nDonec sit amet pulvinar orci. \nQuisque volutpat pharetra tincidunt. Fusce sapien arcu, molestie eget varius egestas, \nfaucibus ac urna. Sed at nisi in velit egestas aliquam ut a felis. Aenean malesuada iaculis nisl, \nut tempor lacus egestas consequat. Nam nibh lectus, gravida sed egestas ut, feugiat quis \ndolor. Donec eu leo enim, non laoreet ante. Morbi dictum tempor vulputate. Phasellus \nultricies risus vel augue sagittis euismod. Vivamus tincidunt placerat nisi in aliquam. Cras \nquis mi ac nunc pretium aliquam. Aenean elementum erat ac metus commodo rhoncus. \nAliquam nulla augue, porta non sagittis quis, accumsan vitae sem. Phasellus id lectus tortor, \neget pulvinar augue. Etiam eget velit ac purus fringilla blandit. Donec odio odio, sagittis sed \niaculis sed, consectetur eget sem. Lorem ipsum dolor sit amet, consectetur adipiscing elit. \nMaecenas accumsan velit vel turpis rutrum in sodales diam placerat. \nQuisque luctus ullamcorper velit sit amet lobortis. Etiam ligula felis, vulputate quis rhoncus \nnec, fermentum eget odio. Vivamus vel ipsum ac augue sodales mollis euismod nec tellus. \nFusce et augue rutrum nunc semper vehicula vel semper nisl. Nam laoreet euismod quam at \nvarius. Sed aliquet auctor nibh. Curabitur malesuada fermentum lacus vel accumsan. Duis \nornare scelerisque nulla, ac pulvinar ligula tempus sit amet. In placerat nulla ac ante \nscelerisque posuere. Phasellus at ante felis. Sed hendrerit risus a metus posuere rutrum. \nPhasellus eu augue dui. Proin in vestibulum ipsum. Aenean accumsan mollis sapien, ut \neleifend sem blandit at. Vivamus luctus mi eget lorem lobortis pharetra. Phasellus at tortor \nquam, a volutpat purus. Etiam sollicitudin arcu vel elit bibendum et imperdiet risus tincidunt. \nEtiam elit velit, posuere ut pulvinar ac, condimentum eget justo. Fusce a erat velit. Vivamus \nimperdiet ultrices orci in hendrerit.","tags":[4],"created":"2022-03-22T07:24:18Z","modified":"2022-03-22T07:24:23.264859Z","added":"2022-03-22T07:24:22.922631Z","archive_serial_number":null,"original_file_name":"2022-03-22 lorem-ipsum.pdf","archived_file_name":"2022-03-22 lorem-ipsum.pdf"},{"id":2,"correspondent":null,"document_type":null,"title":"lorem-ipsum","content":"Test document PDF","tags":[],"created":"2022-03-23T07:24:18Z","modified":"2022-03-23T07:24:23.264859Z","added":"2022-03-23T07:24:22.922631Z","archive_serial_number":null,"original_file_name":"2022-03-23 lorem-ipsum.pdf","archived_file_name":"2022-03-23 lorem-ipsum.pdf"},{"id":3,"correspondent":null,"document_type":1,"title":"lorem-ipsum","content":"Test document PDF","tags":[2],"created":"2022-03-24T07:24:18Z","modified":"2022-03-24T07:24:23.264859Z","added":"2022-03-24T07:24:22.922631Z","archive_serial_number":null,"original_file_name":"2022-03-24 lorem-ipsum.pdf","Line truncated
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 156 KiB |
@@ -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"}]}]}
|
||||
@@ -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}]}
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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) => {}
|
||||
@@ -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) => { ... })
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
Generated
+10626
-4183
File diff suppressed because it is too large.
Load diff
+30
-31
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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!');
|
||||
});
|
||||
});
|
||||
+105
-33
@@ -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)
|
||||
}
|
||||
}
|
||||
+110
-96
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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>
|
||||
File renamed without changes.
+15
-13
@@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -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>
|
||||
File renamed without changes.
+15
-13
@@ -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() {
|
||||
|
||||
+1
-1
@@ -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>
|
||||
File renamed without changes.
+42
@@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -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>
|
||||
|
||||
+2
-4
@@ -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;
|
||||
}
|
||||
}
|
||||
-25
@@ -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();
|
||||
});
|
||||
});
|
||||
+85
-30
@@ -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()) {
|
||||
|
||||
-25
@@ -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();
|
||||
});
|
||||
});
|
||||
+4
-5
@@ -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>
|
||||
|
||||
Loaded 100 of 431 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user