mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-08-03 18:54:40 -05:00
Compare commits
222 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a0ece589b0 | ||
![]() |
8165071edf | ||
![]() |
a667974378 | ||
![]() |
fe1f88ce5d | ||
![]() |
8f2715e437 | ||
![]() |
57a3223c77 | ||
![]() |
df82ac8ac4 | ||
![]() |
16adddc803 | ||
![]() |
d1ae82c5c2 | ||
![]() |
0098936347 | ||
![]() |
17b85f6400 | ||
![]() |
48be9c0fd1 | ||
![]() |
ce36c2d0ea | ||
![]() |
06c63ef4a4 | ||
![]() |
03d93a7d6e | ||
![]() |
a0005c8b3e | ||
![]() |
2220343c50 | ||
![]() |
68fbed996f | ||
![]() |
20a4d8949d | ||
![]() |
1029ecfd49 | ||
![]() |
4de2a3b09e | ||
![]() |
651407c88e | ||
![]() |
3e2e485f66 | ||
![]() |
10efaad224 | ||
![]() |
3dda02660c | ||
![]() |
0b4e8141b0 | ||
![]() |
16ab0efa59 | ||
![]() |
20ca1ec547 | ||
![]() |
a1c9ab237f | ||
![]() |
a65239f7f1 | ||
![]() |
a0622675fd | ||
![]() |
1e0b778097 | ||
![]() |
3b666fef77 | ||
![]() |
9291c98189 | ||
![]() |
f6dadd8c82 | ||
![]() |
022bb272e6 | ||
![]() |
edad1a41e8 | ||
![]() |
421e78a748 | ||
![]() |
b961df90a7 | ||
![]() |
0b26b7098a | ||
![]() |
b09566a9a9 | ||
![]() |
b869deab66 | ||
![]() |
3d552c3112 | ||
![]() |
2a2bf3bf55 | ||
![]() |
26863b8cdc | ||
![]() |
b0f7d07214 | ||
![]() |
30c6557a32 | ||
![]() |
e18e173089 | ||
![]() |
6a8bdbd4f6 | ||
![]() |
b5dec87a62 | ||
![]() |
e50d30876a | ||
![]() |
cbcd9ed67d | ||
![]() |
6bcc26b487 | ||
![]() |
56fcb3fee1 | ||
![]() |
557e1790dd | ||
![]() |
2e67697d36 | ||
![]() |
ca4500692f | ||
![]() |
5aba6bff09 | ||
![]() |
9ed74068b9 | ||
![]() |
be4685742c | ||
![]() |
86b0a38811 | ||
![]() |
3e528f0a9a | ||
![]() |
170d7b6922 | ||
![]() |
f05249a9ad | ||
![]() |
5957ed7af3 | ||
![]() |
8ed63893eb | ||
![]() |
ea9dd926bc | ||
![]() |
f31d3b531f | ||
![]() |
d851448c32 | ||
![]() |
65327d52a6 | ||
![]() |
2ea5ae59b2 | ||
![]() |
a04d09028b | ||
![]() |
dd9255cb81 | ||
![]() |
536d7ecd3e | ||
![]() |
f50aac08df | ||
![]() |
db0f1d2159 | ||
![]() |
d97b565d6c | ||
![]() |
d3071f13d8 | ||
![]() |
768407c1d7 | ||
![]() |
c5d18b03cd | ||
![]() |
5f05b44cde | ||
![]() |
beaa09e9b3 | ||
![]() |
d6960f537b | ||
![]() |
0918eab004 | ||
![]() |
9b16789a17 | ||
![]() |
157240351f | ||
![]() |
6ad3d45d60 | ||
![]() |
b715e4d426 | ||
![]() |
aad5e9e99f | ||
![]() |
2a104ad33f | ||
![]() |
851290ee89 | ||
![]() |
a8c6c55e3b | ||
![]() |
992a647424 | ||
![]() |
c22461a1b6 | ||
![]() |
23fefc3ab7 | ||
![]() |
0beb9f0b5f | ||
![]() |
d376f9e7a3 | ||
![]() |
07e7bcd30b | ||
![]() |
95e86cb649 | ||
![]() |
802e5591ce | ||
![]() |
26d5730ad2 | ||
![]() |
8c7554e081 | ||
![]() |
9f5d47c320 | ||
![]() |
4aa452ce63 | ||
![]() |
7ef81ae10f | ||
![]() |
3628292afa | ||
![]() |
8aa5ecde62 | ||
![]() |
2f149eac9d | ||
![]() |
fcfc705b87 | ||
![]() |
7bd5c010a1 | ||
![]() |
52168d8e61 | ||
![]() |
a3842d9228 | ||
![]() |
452c51bd16 | ||
![]() |
22bedd9957 | ||
![]() |
cb318c723d | ||
![]() |
9a81d3c28e | ||
![]() |
3ca59e3b7a | ||
![]() |
07a12bdf15 | ||
![]() |
62e81d8bf0 | ||
![]() |
e295a41caa | ||
![]() |
c545a80aa3 | ||
![]() |
56f1a0cb51 | ||
![]() |
7218b6da97 | ||
![]() |
83f9f2d387 | ||
![]() |
13a2e38385 | ||
![]() |
996d942387 | ||
![]() |
cc42eb9fab | ||
![]() |
44125be979 | ||
![]() |
c2e9cc9a51 | ||
![]() |
fcd10f2adc | ||
![]() |
93009c1eed | ||
![]() |
d875be60d4 | ||
![]() |
db48d4c576 | ||
![]() |
3293231ad2 | ||
![]() |
aa1f2d3b59 | ||
![]() |
f492b679e3 | ||
![]() |
7ca84322bd | ||
![]() |
e3257b8fa3 | ||
![]() |
0bcda5ded8 | ||
![]() |
7ec82c0891 | ||
![]() |
3241ac7dc2 | ||
![]() |
e974605fc8 | ||
![]() |
ce13380533 | ||
![]() |
e23e3acda3 | ||
![]() |
63ab9972da | ||
![]() |
feb4901620 | ||
![]() |
67788a1b1b | ||
![]() |
001faf9ed7 | ||
![]() |
7a464d8a6e | ||
![]() |
5acd1c7c1b | ||
![]() |
de14540374 | ||
![]() |
37e0c2667b | ||
![]() |
252abb41c3 | ||
![]() |
fb2af341d8 | ||
![]() |
931f5f9c27 | ||
![]() |
b5d04e575e | ||
![]() |
3d395601fe | ||
![]() |
6630ce646c | ||
![]() |
f5508eea1c | ||
![]() |
c8c460432f | ||
![]() |
18299dafd2 | ||
![]() |
817d09026e | ||
![]() |
d76b009390 | ||
![]() |
9effed3ce1 | ||
![]() |
c1bbfc5dcf | ||
![]() |
b6c9cfb76f | ||
![]() |
59ca7bbcf2 | ||
![]() |
52c8d5e999 | ||
![]() |
5851e7f1b7 | ||
![]() |
e05b3441de | ||
![]() |
0d6e79cb93 | ||
![]() |
76a102d901 | ||
![]() |
bbd4659fbf | ||
![]() |
0880420ef6 | ||
![]() |
2351c79282 | ||
![]() |
d6016fc798 | ||
![]() |
bc17291006 | ||
![]() |
ed6cb14c4d | ||
![]() |
08de8a04b8 | ||
![]() |
5c67de8b47 | ||
![]() |
38b0408b1a | ||
![]() |
9ccad7ea86 | ||
![]() |
4a4e810a14 | ||
![]() |
76d2df3bde | ||
![]() |
c02563d894 | ||
![]() |
574ec6780b | ||
![]() |
9e0f56982b | ||
![]() |
1c66daf12b | ||
![]() |
59d683849e | ||
![]() |
9946acb1a0 | ||
![]() |
83a760644d | ||
![]() |
25ccff8640 | ||
![]() |
5c4c5a7794 | ||
![]() |
cb6af97595 | ||
![]() |
c4407dccf6 | ||
![]() |
ecdea4c3c8 | ||
![]() |
26d6f302cf | ||
![]() |
ecf10622ef | ||
![]() |
0fb553675b | ||
![]() |
2080fde4f9 | ||
![]() |
d10e67ce09 | ||
![]() |
74fe7c586b | ||
![]() |
05188aed6d | ||
![]() |
865efb7752 | ||
![]() |
4782b4da07 | ||
![]() |
4693632c7d | ||
![]() |
4c4b571a88 | ||
![]() |
328c87995b | ||
![]() |
a1d10e7d4a | ||
![]() |
77d9a7e9d3 | ||
![]() |
981b090088 | ||
![]() |
6d1c788ee0 | ||
![]() |
02de773d5b | ||
![]() |
2a240d83fd | ||
![]() |
25cdf7916d | ||
![]() |
11b5983a0d | ||
![]() |
4964987245 | ||
![]() |
ed129d6074 | ||
![]() |
37e928d869 | ||
![]() |
06def8c11e | ||
![]() |
10571676a4 | ||
![]() |
af5160237d |
14
.codecov.yml
14
.codecov.yml
@@ -1,3 +1,17 @@
|
||||
codecov:
|
||||
require_ci_to_pass: true
|
||||
# https://docs.codecov.com/docs/flags#recommended-automatic-flag-management
|
||||
# Require each flag to have 1 upload before notification
|
||||
flag_management:
|
||||
default_rules:
|
||||
after_n_builds: 1
|
||||
individual_flags:
|
||||
- name: backend
|
||||
paths:
|
||||
- src/
|
||||
- name: frontend
|
||||
paths:
|
||||
- src-ui/
|
||||
# https://docs.codecov.com/docs/pull-request-comments
|
||||
# codecov will only comment if coverage changes
|
||||
comment:
|
||||
|
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -20,11 +20,16 @@ NOTE: Please check only one box!
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] Other (please explain)
|
||||
- [ ] Other (please explain):
|
||||
|
||||
## Checklist:
|
||||
|
||||
<!--
|
||||
NOTE: PRs that do not address the following will not be merged, please do not skip any relevant items.
|
||||
-->
|
||||
|
||||
- [ ] I have read & agree with the [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/main/CONTRIBUTING.md).
|
||||
- [ ] If applicable, I have included testing coverage for new code in this PR, for [backend](https://docs.paperless-ngx.com/development/#testing) and / or [front-end](https://docs.paperless-ngx.com/development/#testing-and-code-style) changes.
|
||||
- [ ] If applicable, I have tested my code for new features & regressions on both mobile & desktop devices, using the latest version of major browsers.
|
||||
- [ ] If applicable, I have checked that all tests pass, see [documentation](https://docs.paperless-ngx.com/development/#back-end-development).
|
||||
- [ ] I have run all `pre-commit` hooks, see [documentation](https://docs.paperless-ngx.com/development/#code-formatting-with-pre-commit-hooks).
|
||||
|
15
.github/dependabot.yml
vendored
15
.github/dependabot.yml
vendored
@@ -17,6 +17,21 @@ updates:
|
||||
# Add reviewers
|
||||
reviewers:
|
||||
- "paperless-ngx/frontend"
|
||||
groups:
|
||||
frontend-angular-dependencies:
|
||||
patterns:
|
||||
- "@angular*"
|
||||
- "@ng-*"
|
||||
- "ngx-*"
|
||||
- "ng2-pdf-viewer"
|
||||
frontend-jest-dependencies:
|
||||
patterns:
|
||||
- "@types/jest"
|
||||
- "jest"
|
||||
frontend-eslint-dependencies:
|
||||
patterns:
|
||||
- "@typescript-eslint*"
|
||||
- "eslint"
|
||||
|
||||
# Enable version updates for Python
|
||||
- package-ecosystem: "pip"
|
||||
|
23
.github/stale.yml
vendored
23
.github/stale.yml
vendored
@@ -1,23 +0,0 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 30
|
||||
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
|
||||
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
|
||||
onlyLabels: [cant-reproduce]
|
||||
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
|
||||
# See https://github.com/marketplace/stale for more info on the app
|
||||
# and https://github.com/probot/stale for the configuration docs
|
30
.github/workflows/ci.yml
vendored
30
.github/workflows/ci.yml
vendored
@@ -16,7 +16,7 @@ on:
|
||||
env:
|
||||
# This is the version of pipenv all the steps will use
|
||||
# If changing this, change Dockerfile
|
||||
DEFAULT_PIP_ENV_VERSION: "2023.4.20"
|
||||
DEFAULT_PIP_ENV_VERSION: "2023.7.23"
|
||||
# This is the default version of Python to use in most steps
|
||||
# If changing this, change Dockerfile
|
||||
DEFAULT_PYTHON_VERSION: "3.9"
|
||||
@@ -77,6 +77,7 @@ jobs:
|
||||
with:
|
||||
name: documentation
|
||||
path: site/
|
||||
retention-days: 7
|
||||
|
||||
documentation-deploy:
|
||||
name: "Deploy Documentation"
|
||||
@@ -199,15 +200,36 @@ jobs:
|
||||
name: Linting checks
|
||||
run: cd src-ui && npm run lint
|
||||
-
|
||||
name: Run Playwright tests
|
||||
name: Run Jest unit tests
|
||||
run: cd src-ui && npm run test
|
||||
-
|
||||
name: Upload Jest coverage
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: jest-coverage-report
|
||||
path: src-ui/coverage
|
||||
retention-days: 7
|
||||
-
|
||||
name: Upload frontend coverage to Codecov
|
||||
if: always()
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
# not required for public repos, but intermittently fails otherwise
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
# future expansion
|
||||
flags: frontend
|
||||
-
|
||||
name: Run Playwright e2e tests
|
||||
run: cd src-ui && npx playwright test
|
||||
-
|
||||
name: Upload test results
|
||||
name: Upload Playwright test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: playwright-report
|
||||
path: src-ui/playwright-report
|
||||
retention-days: 7
|
||||
|
||||
build-docker-image:
|
||||
name: Build Docker image for ${{ github.ref_name }}
|
||||
@@ -328,6 +350,7 @@ jobs:
|
||||
with:
|
||||
name: frontend-compiled
|
||||
path: src/documents/static/frontend/
|
||||
retention-days: 7
|
||||
|
||||
build-release:
|
||||
needs:
|
||||
@@ -436,6 +459,7 @@ jobs:
|
||||
with:
|
||||
name: release
|
||||
path: dist/paperless-ngx.tar.xz
|
||||
retention-days: 7
|
||||
|
||||
publish-release:
|
||||
runs-on: ubuntu-22.04
|
||||
|
6
.github/workflows/cleanup-tags.yml
vendored
6
.github/workflows/cleanup-tags.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
-
|
||||
name: Clean temporary images
|
||||
if: "${{ env.TOKEN != '' }}"
|
||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.1.0
|
||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.2.0
|
||||
with:
|
||||
token: "${{ env.TOKEN }}"
|
||||
owner: "${{ github.repository_owner }}"
|
||||
@@ -38,6 +38,7 @@ jobs:
|
||||
scheme: "branch"
|
||||
repo_name: "paperless-ngx"
|
||||
match_regex: "feature-"
|
||||
do_delete: "true"
|
||||
|
||||
cleanup-untagged-images:
|
||||
name: Cleanup Untagged Images Tags for ${{ matrix.primary-name }}
|
||||
@@ -67,9 +68,10 @@ jobs:
|
||||
-
|
||||
name: Clean untagged images
|
||||
if: "${{ env.TOKEN != '' }}"
|
||||
uses: stumpylog/image-cleaner-action/untagged@v0.1.0
|
||||
uses: stumpylog/image-cleaner-action/untagged@v0.2.0
|
||||
with:
|
||||
token: "${{ env.TOKEN }}"
|
||||
owner: "${{ github.repository_owner }}"
|
||||
is_org: "true"
|
||||
package_name: "${{ matrix.primary-name }}"
|
||||
do_delete: "true"
|
||||
|
6
.github/workflows/repo-maintenance.yml
vendored
6
.github/workflows/repo-maintenance.yml
vendored
@@ -19,9 +19,9 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/stale@v8
|
||||
with:
|
||||
days-before-stale: 30
|
||||
days-before-close: 7
|
||||
only-labels: 'cant-reproduce'
|
||||
days-before-stale: 7
|
||||
days-before-close: 14
|
||||
any-of-labels: 'cant-reproduce,not a bug'
|
||||
stale-issue-label: stale
|
||||
stale-pr-label: stale
|
||||
stale-issue-message: >
|
||||
|
@@ -27,7 +27,7 @@ repos:
|
||||
- id: check-case-conflict
|
||||
- id: detect-private-key
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: 'v2.7.1'
|
||||
rev: 'v3.0.0'
|
||||
hooks:
|
||||
- id: prettier
|
||||
types_or:
|
||||
@@ -37,11 +37,11 @@ repos:
|
||||
exclude: "(^Pipfile\\.lock$)"
|
||||
# Python hooks
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: 'v0.0.272'
|
||||
rev: 'v0.0.280'
|
||||
hooks:
|
||||
- id: ruff
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.3.0
|
||||
rev: 23.7.0
|
||||
hooks:
|
||||
- id: black
|
||||
# Dockerfile hooks
|
||||
|
@@ -2,3 +2,5 @@
|
||||
semi: false
|
||||
# https://prettier.io/docs/en/options.html#quotes
|
||||
singleQuote: true
|
||||
# https://prettier.io/docs/en/options.html#trailing-commas
|
||||
trailingComma: "es5"
|
||||
|
@@ -2,7 +2,7 @@
|
||||
# https://beta.ruff.rs/docs/rules/
|
||||
extend-select = ["I", "W", "UP", "COM", "DJ", "EXE", "ISC", "ICN", "G201", "INP", "PIE", "RSE", "SIM", "TID", "PLC", "PLE", "RUF"]
|
||||
# TODO PTH
|
||||
ignore = ["DJ001", "SIM105"]
|
||||
ignore = ["DJ001", "SIM105", "RUF012"]
|
||||
fix = true
|
||||
line-length = 88
|
||||
respect-gitignore = true
|
||||
|
@@ -45,7 +45,7 @@ Examples of `non-trivial` PRs might include:
|
||||
|
||||
- Additional features
|
||||
- Large changes to many distinct files
|
||||
- Breaking or depreciation of existing features
|
||||
- Breaking or deprecation of existing features
|
||||
|
||||
Our community review process for `non-trivial` PRs is the following:
|
||||
|
||||
|
15
Dockerfile
15
Dockerfile
@@ -1,4 +1,4 @@
|
||||
# syntax=docker/dockerfile:1.4
|
||||
# syntax=docker/dockerfile:1
|
||||
# https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/reference.md
|
||||
|
||||
# Stage: compile-frontend
|
||||
@@ -29,7 +29,7 @@ COPY Pipfile* ./
|
||||
|
||||
RUN set -eux \
|
||||
&& echo "Installing pipenv" \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2023.4.20 \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2023.7.23 \
|
||||
&& echo "Generating requirement.txt" \
|
||||
&& pipenv requirements > requirements.txt
|
||||
|
||||
@@ -183,7 +183,7 @@ ARG PSYCOPG2_VERSION=2.9.6
|
||||
RUN set -eux \
|
||||
&& echo "Getting binaries" \
|
||||
&& mkdir paperless-ngx \
|
||||
&& curl --fail --silent --show-error --output paperless-ngx.tar.gz --location https://github.com/paperless-ngx/builder/archive/1f0e6665ba1b144f70fd6dfc8d0e8ba3b7a578ee.tar.gz \
|
||||
&& curl --fail --silent --show-error --output paperless-ngx.tar.gz --location https://github.com/paperless-ngx/builder/archive/58bb061b9b3b63009852d6d875f9a305d9ae6ac9.tar.gz \
|
||||
&& tar -xf paperless-ngx.tar.gz --directory paperless-ngx --strip-components=1 \
|
||||
&& cd paperless-ngx \
|
||||
# Setting a specific revision ensures we know what this installed
|
||||
@@ -214,15 +214,18 @@ COPY --from=pipenv-base /usr/src/pipenv/requirements.txt ./
|
||||
ARG BUILD_PACKAGES="\
|
||||
build-essential \
|
||||
git \
|
||||
default-libmysqlclient-dev"
|
||||
default-libmysqlclient-dev \
|
||||
pkg-config"
|
||||
|
||||
RUN set -eux \
|
||||
# hadolint ignore=DL3042
|
||||
RUN --mount=type=cache,target=/root/.cache/pip/,id=pip-cache \
|
||||
set -eux \
|
||||
&& echo "Installing build system packages" \
|
||||
&& apt-get update \
|
||||
&& apt-get install --yes --quiet --no-install-recommends ${BUILD_PACKAGES} \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade wheel \
|
||||
&& echo "Installing Python requirements" \
|
||||
&& python3 -m pip install --default-timeout=1000 --no-cache-dir --requirement requirements.txt \
|
||||
&& python3 -m pip install --default-timeout=1000 --requirement requirements.txt \
|
||||
&& echo "Installing NLTK data" \
|
||||
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" snowball_data \
|
||||
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" stopwords \
|
||||
|
15
Pipfile
15
Pipfile
@@ -18,7 +18,7 @@ django-celery-results = "*"
|
||||
django-compression-middleware = "*"
|
||||
django-guardian = "*"
|
||||
django-extensions = "*"
|
||||
django-filter = "~=22.1"
|
||||
django-filter = "~=23.1"
|
||||
djangorestframework = "~=3.14"
|
||||
djangorestframework-guardian = "*"
|
||||
filelock = "*"
|
||||
@@ -26,8 +26,6 @@ gunicorn = "*"
|
||||
imap-tools = "*"
|
||||
langdetect = "*"
|
||||
pathvalidate = "*"
|
||||
pillow = "*"
|
||||
pikepdf = "*"
|
||||
python-gnupg = "*"
|
||||
python-dotenv = "*"
|
||||
python-dateutil = "*"
|
||||
@@ -36,9 +34,9 @@ python-ipware = "*"
|
||||
psycopg2 = "*"
|
||||
rapidfuzz = "*"
|
||||
redis = {extras = ["hiredis"], version = "*"}
|
||||
scikit-learn = "~=1.2"
|
||||
whitenoise = "~=6.3"
|
||||
watchdog = "~=2.2"
|
||||
scikit-learn = "~=1.3"
|
||||
whitenoise = "~=6.5"
|
||||
watchdog = "~=3.0"
|
||||
whoosh="~=2.7"
|
||||
inotifyrecursive = "~=0.3"
|
||||
ocrmypdf = "~=14.0"
|
||||
@@ -64,8 +62,10 @@ zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"}
|
||||
scipy = "==1.8.1"
|
||||
# v4 brings in extra dependencies for features not used here
|
||||
reportlab = "==3.6.12"
|
||||
# Pin this until piwheels is building a newer version (see https://www.piwheels.org/project/cryptography/)
|
||||
# Pin these until piwheels is building a newer version (see https://www.piwheels.org/project/{package}/)
|
||||
cryptography = "==40.0.1"
|
||||
pikepdf = "==7.2.0"
|
||||
pillow = "==9.5.0"
|
||||
|
||||
[dev-packages]
|
||||
# Linting
|
||||
@@ -81,6 +81,7 @@ pytest-httpx = "*"
|
||||
pytest-env = "*"
|
||||
pytest-sugar = "*"
|
||||
pytest-xdist = "*"
|
||||
pytest-rerunfailures = "*"
|
||||
"pdfminer.six" = "*"
|
||||
imagehash = "*"
|
||||
daphne = "*"
|
||||
|
1972
Pipfile.lock
generated
1972
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -37,7 +37,7 @@ services:
|
||||
- redisdata:/data
|
||||
|
||||
db:
|
||||
image: docker.io/library/postgres:13
|
||||
image: docker.io/library/postgres:15
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
@@ -39,7 +39,7 @@ services:
|
||||
- redisdata:/data
|
||||
|
||||
db:
|
||||
image: docker.io/library/postgres:13
|
||||
image: docker.io/library/postgres:15
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
@@ -35,7 +35,7 @@ services:
|
||||
- redisdata:/data
|
||||
|
||||
db:
|
||||
image: docker.io/library/postgres:13
|
||||
image: docker.io/library/postgres:15
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
@@ -15,6 +15,7 @@ stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
environment = HOME="/usr/src/paperless",USER="paperless"
|
||||
|
||||
[program:consumer]
|
||||
command=python3 manage.py document_consumer
|
||||
@@ -25,6 +26,7 @@ stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
environment = HOME="/usr/src/paperless",USER="paperless"
|
||||
|
||||
[program:celery]
|
||||
|
||||
@@ -37,6 +39,7 @@ stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
environment = HOME="/usr/src/paperless",USER="paperless"
|
||||
|
||||
[program:celery-beat]
|
||||
|
||||
@@ -48,6 +51,7 @@ stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
environment = HOME="/usr/src/paperless",USER="paperless"
|
||||
|
||||
[program:celery-flower]
|
||||
command = /usr/local/bin/flower-conditional.sh
|
||||
@@ -58,3 +62,4 @@ stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
environment = HOME="/usr/src/paperless",USER="paperless"
|
||||
|
@@ -68,23 +68,23 @@ $ docker-compose down
|
||||
|
||||
After that, [make a backup](#backup).
|
||||
|
||||
1. If you pull the image from the docker hub, all you need to do is:
|
||||
1. If you pull the image from the docker hub, all you need to do is:
|
||||
|
||||
```shell-session
|
||||
$ docker-compose pull
|
||||
$ docker-compose up
|
||||
```
|
||||
```shell-session
|
||||
$ docker-compose pull
|
||||
$ docker-compose up
|
||||
```
|
||||
|
||||
The docker-compose files refer to the `latest` version, which is
|
||||
always the latest stable release.
|
||||
The docker-compose files refer to the `latest` version, which is
|
||||
always the latest stable release.
|
||||
|
||||
2. If you built the image yourself, do the following:
|
||||
1. If you built the image yourself, do the following:
|
||||
|
||||
```shell-session
|
||||
$ git pull
|
||||
$ docker-compose build
|
||||
$ docker-compose up
|
||||
```
|
||||
```shell-session
|
||||
$ git pull
|
||||
$ docker-compose build
|
||||
$ docker-compose up
|
||||
```
|
||||
|
||||
Running `docker-compose up` will also apply any new database migrations.
|
||||
If you see everything working, press CTRL+C once to gracefully stop
|
||||
@@ -167,6 +167,16 @@ following:
|
||||
This might not actually do anything. Not every new paperless version
|
||||
comes with new database migrations.
|
||||
|
||||
### Database Upgrades
|
||||
|
||||
In general, paperless does not require a specific version of PostgreSQL or MariaDB and it is
|
||||
safe to update them to newer versions. However, you should always take a backup and follow
|
||||
the instructions from your database's documentation for how to upgrade between major versions.
|
||||
|
||||
For PostgreSQL, refer to [Upgrading a PostgreSQL Cluster](https://www.postgresql.org/docs/current/upgrading.html).
|
||||
|
||||
For MariaDB, refer to [Upgrading MariaDB](https://mariadb.com/kb/en/upgrading/)
|
||||
|
||||
## Downgrading Paperless {#downgrade-paperless}
|
||||
|
||||
Downgrades are possible. However, some updates also contain database
|
||||
@@ -460,7 +470,7 @@ The issues detected by the sanity checker are as follows:
|
||||
- Inaccessible thumbnails due to improper permissions.
|
||||
- Documents without any content (warning).
|
||||
- Orphaned files in the media directory (warning). These are files
|
||||
that are not referenced by any document im paperless.
|
||||
that are not referenced by any document in paperless.
|
||||
|
||||
```
|
||||
document_sanity_checker
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Advanced Topics
|
||||
|
||||
Paperless offers a couple features that automate certain tasks and make
|
||||
Paperless offers a couple of features that automate certain tasks and make
|
||||
your life easier.
|
||||
|
||||
## Matching tags, correspondents, document types, and storage paths {#matching}
|
||||
@@ -35,9 +35,9 @@ The following algorithms are available:
|
||||
(i.e. preserve ordering) in the PDF.
|
||||
- **Regular expression:** Parses the match as a regular expression and
|
||||
tries to find a match within the document.
|
||||
- **Fuzzy match:** I don't know. Look at the source.
|
||||
- **Fuzzy match:** I don't know. Look at [the source](https://github.com/paperless-ngx/paperless-ngx/blob/main/src/documents/matching.py).
|
||||
- **Auto:** Tries to automatically match new documents. This does not
|
||||
require you to set a match. See the notes below.
|
||||
require you to set a match. See the [notes below](#automatic-matching).
|
||||
|
||||
When using the _any_ or _all_ matching algorithms, you can search for
|
||||
terms that consist of multiple words by enclosing them in double quotes.
|
||||
@@ -92,7 +92,7 @@ when using this feature:
|
||||
decide when not to assign a certain tag, correspondent, document
|
||||
type, or storage path. This will usually be the case as you start
|
||||
filling up paperless with documents. Example: If all your documents
|
||||
are either from "Webshop" and "Bank", paperless will assign one
|
||||
are either from "Webshop" or "Bank", paperless will assign one
|
||||
of these correspondents to ANY new document, if both are set to
|
||||
automatic matching.
|
||||
|
||||
@@ -101,7 +101,7 @@ when using this feature:
|
||||
Sometimes you may want to do something arbitrary whenever a document is
|
||||
consumed. Rather than try to predict what you may want to do, Paperless
|
||||
lets you execute scripts of your own choosing just before or after a
|
||||
document is consumed using a couple simple hooks.
|
||||
document is consumed using a couple of simple hooks.
|
||||
|
||||
Just write a script, put it somewhere that Paperless can read & execute,
|
||||
and then put the path to that script in `paperless.conf` or
|
||||
@@ -197,7 +197,7 @@ The script can be in any language, A simple shell script example:
|
||||
!!! warning
|
||||
|
||||
The post consumption script should not modify the document files
|
||||
directly
|
||||
directly.
|
||||
|
||||
The script's stdout and stderr will be logged line by line to the
|
||||
webserver log, along with the exit code of the script.
|
||||
@@ -311,6 +311,7 @@ Paperless provides the following placeholders within filenames:
|
||||
- `{added_day}`: Day added only (number 01-31).
|
||||
- `{owner_username}`: Username of document owner, if any, or "none"
|
||||
- `{original_name}`: Document original filename, minus the extension, if any, or "none"
|
||||
- `{doc_pk}`: The paperless identifier (primary key) for the document.
|
||||
|
||||
Paperless will try to conserve the information from your database as
|
||||
much as possible. However, some characters that you can use in document
|
||||
@@ -528,7 +529,7 @@ For how to enable barcode usage, see [the configuration](/configuration#barcodes
|
||||
The two settings may be enabled independently, but do have interactions as explained
|
||||
below.
|
||||
|
||||
### Document Splitting
|
||||
### Document Splitting {#document-splitting}
|
||||
|
||||
When enabled, Paperless will look for a barcode with the configured value and create a new document
|
||||
starting from the next page. The page with the barcode on it will _not_ be retained. It
|
||||
@@ -543,3 +544,75 @@ If document splitting via barcode is also enabled, documents will be split when
|
||||
barcode is located. However, differing from the splitting, the page with the
|
||||
barcode _will_ be retained. This allows application of a barcode to any page, including
|
||||
one which holds data to keep in the document.
|
||||
|
||||
## Automatic collation of double-sided documents {#collate}
|
||||
|
||||
!!! note
|
||||
|
||||
If your scanner supports double-sided scanning natively, you do not need this feature.
|
||||
|
||||
This feature is turned off by default, see [configuration](/configuration#collate) on how to turn it on.
|
||||
|
||||
### Summary
|
||||
|
||||
If you have a scanner with an automatic document feeder (ADF) that only scans a single side,
|
||||
this feature makes scanning double-sided documents much more convenient by automatically
|
||||
collating two separate scans into one document, reordering the pages as necessary.
|
||||
|
||||
### Usage example
|
||||
|
||||
Suppose you have a double-sided document with 6 pages (3 sheets of paper). First,
|
||||
put the stack into your ADF as normal, ensuring that page 1 is scanned first. Your ADF
|
||||
will now scan pages 1, 3, and 5. Then you (or your the scanner, if it supports it) upload
|
||||
the scan into the correct sub-directory of the consume folder (`double-sided` by default;
|
||||
keep in mind that Paperless will _not_ automatically create the directory for you.)
|
||||
Paperless will then process the scan and move it into an internal staging area.
|
||||
|
||||
The next step is to turn your stack upside down (without reordering the sheets of paper),
|
||||
and scan it once again, your ADF will now scan pages 6, 4, and 2, in that order. Once this
|
||||
scan is copied into the sub-directory, Paperless will collate the previous scan with the
|
||||
new one, reversing the order of the pages on the second, "even numbered" scan. The
|
||||
resulting document will have the pages 1-6 in the correct order, and this new file will
|
||||
then be processed as normal.
|
||||
|
||||
!!! tip
|
||||
|
||||
When scanning the even numbered pages, you can omit the last empty pages, if there are
|
||||
any. For example, if page 6 is empty, you only need to scan pages 2 and 4. _Do not_ omit
|
||||
empty pages in the middle of the document.
|
||||
|
||||
### Things that could go wrong
|
||||
|
||||
Paperless will notice when the first, "odd numbered" scan has less pages than the second
|
||||
scan (this can happen when e.g. the ADF skipped a few pages in the first pass). In that
|
||||
case, Paperless will remove the staging copy as well as the scan, and give you an error
|
||||
message asking you to restart the process from scratch, by scanning the odd pages again,
|
||||
followed by the even pages.
|
||||
|
||||
It's important that the scan files get consumed in the correct order, and one at a time.
|
||||
You therefore need to make sure that Paperless is running while you upload the files into
|
||||
the directory; and if you're using [polling](/configuration#polling), make sure that
|
||||
`CONSUMER_POLLING` is set to a value lower than it takes for the second scan to appear,
|
||||
like 5-10 or even lower.
|
||||
|
||||
Another thing that might happen is that you start a double sided scan, but then forget
|
||||
to upload the second file. To avoid collating the wrong documents if you then come back
|
||||
a day later to scan a new double-sided document, Paperless will only keep an "odd numbered
|
||||
pages" file for up to 30 minutes. If more time passes, it will consider the next incoming
|
||||
scan a completely new "odd numbered pages" one. The old staging file will get discarded.
|
||||
|
||||
### Interaction with "subdirs as tags"
|
||||
|
||||
The collation feature can be used together with the [subdirs as tags](/configuration#consume_config)
|
||||
feature (but this is not a requirement). Just create a correctly named double-sided subdir
|
||||
in the hierachy and upload your scans there. For example, both `double-sided/foo/bar` as
|
||||
well as `foo/bar/double-sided` will cause the collated document to be treated as if it
|
||||
were uploaded into `foo/bar` and receive both `foo` and `bar` tags, but not `double-sided`.
|
||||
|
||||
### Interaction with document splitting
|
||||
|
||||
You can use the [document splitting](#document-splitting) feature, but if you use a normal
|
||||
single-sided split marker page, the split document(s) will have an empty page at the front (or
|
||||
whatever else was on the backside of the split marker page.) You can work around that by having
|
||||
a split marker page that has the split barcode on _both_ sides. This way, the extra page will
|
||||
get automatically removed.
|
||||
|
13
docs/api.md
13
docs/api.md
@@ -288,10 +288,23 @@ with an optional `set_permissions` parameter which is of the form:
|
||||
}
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
Arrays should contain user or group ID numbers.
|
||||
|
||||
If this parameter is supplied the object's permissions will be overwritten,
|
||||
assuming the authenticated user has permission to do so (the user must be
|
||||
the object owner or a superuser).
|
||||
|
||||
### Retrieving full permissions
|
||||
|
||||
By default, the API will return a truncated version of object-level
|
||||
permissions, returning `user_can_change` indicating whether the current user
|
||||
can edit the object (either because they are the object owner or have permissions
|
||||
granted). You can pass the parameter `full_perms=true` to API calls to view the
|
||||
full permissions of objects in a format that mirrors the `set_permissions`
|
||||
parameter above.
|
||||
|
||||
## API Versioning
|
||||
|
||||
The REST API is versioned since Paperless-ngx 1.3.0.
|
||||
|
@@ -1,5 +1,286 @@
|
||||
# Changelog
|
||||
|
||||
## paperless-ngx 1.17.1
|
||||
|
||||
### Features
|
||||
|
||||
- Fix / Enhancement: restrict status messages by owner if set \& improve 404 page [@shamoon](https://github.com/shamoon) ([#3959](https://github.com/paperless-ngx/paperless-ngx/pull/3959))
|
||||
- Feature: Add Ukrainian translation [@shamoon](https://github.com/shamoon) ([#3941](https://github.com/paperless-ngx/paperless-ngx/pull/3941))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: handle ASN = 0 on frontend cards [@shamoon](https://github.com/shamoon) ([#3988](https://github.com/paperless-ngx/paperless-ngx/pull/3988))
|
||||
- Fix: improve light color filled primary button text legibility [@shamoon](https://github.com/shamoon) ([#3980](https://github.com/paperless-ngx/paperless-ngx/pull/3980))
|
||||
- Fix / Enhancement: restrict status messages by owner if set \& improve 404 page [@shamoon](https://github.com/shamoon) ([#3959](https://github.com/paperless-ngx/paperless-ngx/pull/3959))
|
||||
- Fix: handle very old date strings in correspondent list [@shamoon](https://github.com/shamoon) ([#3953](https://github.com/paperless-ngx/paperless-ngx/pull/3953))
|
||||
|
||||
### Documentation
|
||||
|
||||
- docs(bare-metal): add new dependency [@bin101](https://github.com/bin101) ([#3931](https://github.com/paperless-ngx/paperless-ngx/pull/3931))
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Chore: Loosen Pipfile restriction on some packages and update them [@stumpylog](https://github.com/stumpylog) ([#3972](https://github.com/paperless-ngx/paperless-ngx/pull/3972))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>6 changes</summary>
|
||||
|
||||
- Fix: handle ASN = 0 on frontend cards [@shamoon](https://github.com/shamoon) ([#3988](https://github.com/paperless-ngx/paperless-ngx/pull/3988))
|
||||
- Fix: improve light color filled primary button text legibility [@shamoon](https://github.com/shamoon) ([#3980](https://github.com/paperless-ngx/paperless-ngx/pull/3980))
|
||||
- Fix / Enhancement: restrict status messages by owner if set \& improve 404 page [@shamoon](https://github.com/shamoon) ([#3959](https://github.com/paperless-ngx/paperless-ngx/pull/3959))
|
||||
- Fix: handle very old date strings in correspondent list [@shamoon](https://github.com/shamoon) ([#3953](https://github.com/paperless-ngx/paperless-ngx/pull/3953))
|
||||
- Chore: Reduces the 2 mail tests flakiness [@stumpylog](https://github.com/stumpylog) ([#3949](https://github.com/paperless-ngx/paperless-ngx/pull/3949))
|
||||
- Feature: Add Ukrainian translation [@shamoon](https://github.com/shamoon) ([#3941](https://github.com/paperless-ngx/paperless-ngx/pull/3941))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 1.17.0
|
||||
|
||||
### Features
|
||||
|
||||
- Add support for additional UK date formats [@brainrecursion](https://github.com/brainrecursion) ([#3887](https://github.com/paperless-ngx/paperless-ngx/pull/3887))
|
||||
- Add 'doc_pk' to PAPERLESS_FILENAME_FORMAT handling [@mechanarchy](https://github.com/mechanarchy) ([#3861](https://github.com/paperless-ngx/paperless-ngx/pull/3861))
|
||||
- Feature: hover buttons for saved view widgets [@shamoon](https://github.com/shamoon) ([#3875](https://github.com/paperless-ngx/paperless-ngx/pull/3875))
|
||||
- Feature: collate two single-sided multipage scans [@brakhane](https://github.com/brakhane) ([#3784](https://github.com/paperless-ngx/paperless-ngx/pull/3784))
|
||||
- Feature: include global and object-level permissions in export / import [@shamoon](https://github.com/shamoon) ([#3672](https://github.com/paperless-ngx/paperless-ngx/pull/3672))
|
||||
- Enhancement / Fix: Migrate encrypted png thumbnails to webp [@shamoon](https://github.com/shamoon) ([#3719](https://github.com/paperless-ngx/paperless-ngx/pull/3719))
|
||||
- Feature: Add Slovak translation [@shamoon](https://github.com/shamoon) ([#3722](https://github.com/paperless-ngx/paperless-ngx/pull/3722))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: cancel possibly slow queries on doc details [@shamoon](https://github.com/shamoon) ([#3925](https://github.com/paperless-ngx/paperless-ngx/pull/3925))
|
||||
- Fix: note creation / deletion should respect doc permissions [@shamoon](https://github.com/shamoon) ([#3903](https://github.com/paperless-ngx/paperless-ngx/pull/3903))
|
||||
- Fix: notes show persistent scrollbars [@shamoon](https://github.com/shamoon) ([#3904](https://github.com/paperless-ngx/paperless-ngx/pull/3904))
|
||||
- Fix: Provide SSL context to IMAP client [@stumpylog](https://github.com/stumpylog) ([#3886](https://github.com/paperless-ngx/paperless-ngx/pull/3886))
|
||||
- Fix/enhancement: permissions for mail rules \& accounts [@shamoon](https://github.com/shamoon) ([#3869](https://github.com/paperless-ngx/paperless-ngx/pull/3869))
|
||||
- Fix: Classifier special case when no items are set to automatic matching [@stumpylog](https://github.com/stumpylog) ([#3858](https://github.com/paperless-ngx/paperless-ngx/pull/3858))
|
||||
- Fix: issues with copy2 or copystat and SELinux permissions [@stumpylog](https://github.com/stumpylog) ([#3847](https://github.com/paperless-ngx/paperless-ngx/pull/3847))
|
||||
- Fix: Parsing office document timestamps [@stumpylog](https://github.com/stumpylog) ([#3836](https://github.com/paperless-ngx/paperless-ngx/pull/3836))
|
||||
- Fix: Add warning to install script need for permissions [@shamoon](https://github.com/shamoon) ([#3835](https://github.com/paperless-ngx/paperless-ngx/pull/3835))
|
||||
- Fix interaction between API and barcode archive serial number [@stumpylog](https://github.com/stumpylog) ([#3834](https://github.com/paperless-ngx/paperless-ngx/pull/3834))
|
||||
- Enhancement / Fix: Migrate encrypted png thumbnails to webp [@shamoon](https://github.com/shamoon) ([#3719](https://github.com/paperless-ngx/paperless-ngx/pull/3719))
|
||||
- Fix: add UI tour step padding [@hakimio](https://github.com/hakimio) ([#3791](https://github.com/paperless-ngx/paperless-ngx/pull/3791))
|
||||
- Fix: translate file tasks types in footer [@shamoon](https://github.com/shamoon) ([#3749](https://github.com/paperless-ngx/paperless-ngx/pull/3749))
|
||||
- Fix: limit ng-select size for addition of filter button [@shamoon](https://github.com/shamoon) ([#3731](https://github.com/paperless-ngx/paperless-ngx/pull/3731))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Documentation: improvements to grammar, spelling, indentation [@mechanarchy](https://github.com/mechanarchy) ([#3844](https://github.com/paperless-ngx/paperless-ngx/pull/3844))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Bump stumpylog/image-cleaner-action from 0.1.0 to 0.2.0 [@dependabot](https://github.com/dependabot) ([#3910](https://github.com/paperless-ngx/paperless-ngx/pull/3910))
|
||||
- Chore: group frontend angular dependabot updates [@shamoon](https://github.com/shamoon) ([#3750](https://github.com/paperless-ngx/paperless-ngx/pull/3750))
|
||||
|
||||
### Dependencies
|
||||
|
||||
<details>
|
||||
<summary>17 changes</summary>
|
||||
|
||||
- Chore: Bump the frontend-angular-dependencies group in /src-ui with 11 updates [@shamoon](https://github.com/shamoon) ([#3918](https://github.com/paperless-ngx/paperless-ngx/pull/3918))
|
||||
- Bump stumpylog/image-cleaner-action from 0.1.0 to 0.2.0 [@dependabot](https://github.com/dependabot) ([#3910](https://github.com/paperless-ngx/paperless-ngx/pull/3910))
|
||||
- Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#3911](https://github.com/paperless-ngx/paperless-ngx/pull/3911))
|
||||
- Bump tslib from 2.6.0 to 2.6.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#3909](https://github.com/paperless-ngx/paperless-ngx/pull/3909))
|
||||
- Bump jest-environment-jsdom from 29.5.0 to 29.6.2 in /src-ui [@dependabot](https://github.com/dependabot) ([#3916](https://github.com/paperless-ngx/paperless-ngx/pull/3916))
|
||||
- Bump [@<!---->types/node from 20.3.3 to 20.4.5 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.3.3 to 20.4.5 in /src-ui @dependabot) ([#3915](https://github.com/paperless-ngx/paperless-ngx/pull/3915))
|
||||
- Bump bootstrap from 5.3.0 to 5.3.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#3914](https://github.com/paperless-ngx/paperless-ngx/pull/3914))
|
||||
- Bump [@<!---->playwright/test from 1.36.1 to 1.36.2 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.36.1 to 1.36.2 in /src-ui @dependabot) ([#3912](https://github.com/paperless-ngx/paperless-ngx/pull/3912))
|
||||
- Bump the frontend-jest-dependencies group in /src-ui with 1 update [@dependabot](https://github.com/dependabot) ([#3906](https://github.com/paperless-ngx/paperless-ngx/pull/3906))
|
||||
- Chore: Update dependencies [@stumpylog](https://github.com/stumpylog) ([#3883](https://github.com/paperless-ngx/paperless-ngx/pull/3883))
|
||||
- Chore: Update Python dependencies [@stumpylog](https://github.com/stumpylog) ([#3842](https://github.com/paperless-ngx/paperless-ngx/pull/3842))
|
||||
- Bump the frontend-angular-dependencies group in /src-ui with 16 updates [@dependabot](https://github.com/dependabot) ([#3826](https://github.com/paperless-ngx/paperless-ngx/pull/3826))
|
||||
- Bump [@<!---->typescript-eslint/eslint-plugin from 5.60.1 to 6.1.0 in /src-ui @dependabot](https://github.com/<!---->typescript-eslint/eslint-plugin from 5.60.1 to 6.1.0 in /src-ui @dependabot) ([#3829](https://github.com/paperless-ngx/paperless-ngx/pull/3829))
|
||||
- Bump jest and [@<!---->types/jest in /src-ui @dependabot](https://github.com/<!---->types/jest in /src-ui @dependabot) ([#3828](https://github.com/paperless-ngx/paperless-ngx/pull/3828))
|
||||
- Bump [@<!---->playwright/test from 1.36.0 to 1.36.1 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.36.0 to 1.36.1 in /src-ui @dependabot) ([#3827](https://github.com/paperless-ngx/paperless-ngx/pull/3827))
|
||||
- Bump semver from 5.7.1 to 5.7.2 in /src-ui [@dependabot](https://github.com/dependabot) ([#3793](https://github.com/paperless-ngx/paperless-ngx/pull/3793))
|
||||
- Chore: Bump Angular to v16 and other frontend packages [@dependabot](https://github.com/dependabot) ([#3727](https://github.com/paperless-ngx/paperless-ngx/pull/3727))
|
||||
</details>
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>35 changes</summary>
|
||||
|
||||
- Fix: cancel possibly slow queries on doc details [@shamoon](https://github.com/shamoon) ([#3925](https://github.com/paperless-ngx/paperless-ngx/pull/3925))
|
||||
- [BUG] Set office document creation date with timezone, if it is naive [@a17t](https://github.com/a17t) ([#3760](https://github.com/paperless-ngx/paperless-ngx/pull/3760))
|
||||
- Fix: note creation / deletion should respect doc permissions [@shamoon](https://github.com/shamoon) ([#3903](https://github.com/paperless-ngx/paperless-ngx/pull/3903))
|
||||
- Chore: Bump the frontend-angular-dependencies group in /src-ui with 11 updates [@shamoon](https://github.com/shamoon) ([#3918](https://github.com/paperless-ngx/paperless-ngx/pull/3918))
|
||||
- Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#3911](https://github.com/paperless-ngx/paperless-ngx/pull/3911))
|
||||
- Bump tslib from 2.6.0 to 2.6.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#3909](https://github.com/paperless-ngx/paperless-ngx/pull/3909))
|
||||
- Bump jest-environment-jsdom from 29.5.0 to 29.6.2 in /src-ui [@dependabot](https://github.com/dependabot) ([#3916](https://github.com/paperless-ngx/paperless-ngx/pull/3916))
|
||||
- Bump [@<!---->types/node from 20.3.3 to 20.4.5 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.3.3 to 20.4.5 in /src-ui @dependabot) ([#3915](https://github.com/paperless-ngx/paperless-ngx/pull/3915))
|
||||
- Bump bootstrap from 5.3.0 to 5.3.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#3914](https://github.com/paperless-ngx/paperless-ngx/pull/3914))
|
||||
- Bump [@<!---->playwright/test from 1.36.1 to 1.36.2 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.36.1 to 1.36.2 in /src-ui @dependabot) ([#3912](https://github.com/paperless-ngx/paperless-ngx/pull/3912))
|
||||
- Bump the frontend-jest-dependencies group in /src-ui with 1 update [@dependabot](https://github.com/dependabot) ([#3906](https://github.com/paperless-ngx/paperless-ngx/pull/3906))
|
||||
- Fix: notes show persistent scrollbars [@shamoon](https://github.com/shamoon) ([#3904](https://github.com/paperless-ngx/paperless-ngx/pull/3904))
|
||||
- Add support for additional UK date formats [@brainrecursion](https://github.com/brainrecursion) ([#3887](https://github.com/paperless-ngx/paperless-ngx/pull/3887))
|
||||
- Add 'doc_pk' to PAPERLESS_FILENAME_FORMAT handling [@mechanarchy](https://github.com/mechanarchy) ([#3861](https://github.com/paperless-ngx/paperless-ngx/pull/3861))
|
||||
- Fix: Provide SSL context to IMAP client [@stumpylog](https://github.com/stumpylog) ([#3886](https://github.com/paperless-ngx/paperless-ngx/pull/3886))
|
||||
- Feature: hover buttons for saved view widgets [@shamoon](https://github.com/shamoon) ([#3875](https://github.com/paperless-ngx/paperless-ngx/pull/3875))
|
||||
- Fix/enhancement: permissions for mail rules \& accounts [@shamoon](https://github.com/shamoon) ([#3869](https://github.com/paperless-ngx/paperless-ngx/pull/3869))
|
||||
- Chore: typing improvements [@stumpylog](https://github.com/stumpylog) ([#3860](https://github.com/paperless-ngx/paperless-ngx/pull/3860))
|
||||
- Fix: Classifier special case when no items are set to automatic matching [@stumpylog](https://github.com/stumpylog) ([#3858](https://github.com/paperless-ngx/paperless-ngx/pull/3858))
|
||||
- Fix: issues with copy2 or copystat and SELinux permissions [@stumpylog](https://github.com/stumpylog) ([#3847](https://github.com/paperless-ngx/paperless-ngx/pull/3847))
|
||||
- Chore: Update Python dependencies [@stumpylog](https://github.com/stumpylog) ([#3842](https://github.com/paperless-ngx/paperless-ngx/pull/3842))
|
||||
- Feature: include global and object-level permissions in export / import [@shamoon](https://github.com/shamoon) ([#3672](https://github.com/paperless-ngx/paperless-ngx/pull/3672))
|
||||
- Fix: Parsing office document timestamps [@stumpylog](https://github.com/stumpylog) ([#3836](https://github.com/paperless-ngx/paperless-ngx/pull/3836))
|
||||
- Fix interaction between API and barcode archive serial number [@stumpylog](https://github.com/stumpylog) ([#3834](https://github.com/paperless-ngx/paperless-ngx/pull/3834))
|
||||
- Bump the frontend-angular-dependencies group in /src-ui with 16 updates [@dependabot](https://github.com/dependabot) ([#3826](https://github.com/paperless-ngx/paperless-ngx/pull/3826))
|
||||
- Enhancement / Fix: Migrate encrypted png thumbnails to webp [@shamoon](https://github.com/shamoon) ([#3719](https://github.com/paperless-ngx/paperless-ngx/pull/3719))
|
||||
- Bump [@<!---->typescript-eslint/eslint-plugin from 5.60.1 to 6.1.0 in /src-ui @dependabot](https://github.com/<!---->typescript-eslint/eslint-plugin from 5.60.1 to 6.1.0 in /src-ui @dependabot) ([#3829](https://github.com/paperless-ngx/paperless-ngx/pull/3829))
|
||||
- Bump jest and [@<!---->types/jest in /src-ui @dependabot](https://github.com/<!---->types/jest in /src-ui @dependabot) ([#3828](https://github.com/paperless-ngx/paperless-ngx/pull/3828))
|
||||
- Bump [@<!---->playwright/test from 1.36.0 to 1.36.1 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.36.0 to 1.36.1 in /src-ui @dependabot) ([#3827](https://github.com/paperless-ngx/paperless-ngx/pull/3827))
|
||||
- Bump semver from 5.7.1 to 5.7.2 in /src-ui [@dependabot](https://github.com/dependabot) ([#3793](https://github.com/paperless-ngx/paperless-ngx/pull/3793))
|
||||
- Fix: add UI tour step padding [@hakimio](https://github.com/hakimio) ([#3791](https://github.com/paperless-ngx/paperless-ngx/pull/3791))
|
||||
- Fix: translate file tasks types in footer [@shamoon](https://github.com/shamoon) ([#3749](https://github.com/paperless-ngx/paperless-ngx/pull/3749))
|
||||
- Feature: Add Slovak translation [@shamoon](https://github.com/shamoon) ([#3722](https://github.com/paperless-ngx/paperless-ngx/pull/3722))
|
||||
- Fix: limit ng-select size for addition of filter button [@shamoon](https://github.com/shamoon) ([#3731](https://github.com/paperless-ngx/paperless-ngx/pull/3731))
|
||||
- Chore: Bump Angular to v16 and other frontend packages [@dependabot](https://github.com/dependabot) ([#3727](https://github.com/paperless-ngx/paperless-ngx/pull/3727))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 1.16.5
|
||||
|
||||
### Features
|
||||
|
||||
- Feature: support barcode upscaling for better detection of small barcodes [@bmachek](https://github.com/bmachek) ([#3655](https://github.com/paperless-ngx/paperless-ngx/pull/3655))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: owner removed when set_permissions passed on object create [@shamoon](https://github.com/shamoon) ([#3702](https://github.com/paperless-ngx/paperless-ngx/pull/3702))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>2 changes</summary>
|
||||
|
||||
- Feature: support barcode upscaling for better detection of small barcodes [@bmachek](https://github.com/bmachek) ([#3655](https://github.com/paperless-ngx/paperless-ngx/pull/3655))
|
||||
- Fix: owner removed when set_permissions passed on object create [@shamoon](https://github.com/shamoon) ([#3702](https://github.com/paperless-ngx/paperless-ngx/pull/3702))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 1.16.4
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: prevent button wrapping when sidebar narrows in MS Edge [@shamoon](https://github.com/shamoon) ([#3682](https://github.com/paperless-ngx/paperless-ngx/pull/3682))
|
||||
- Fix: Handling for filenames with non-ascii and no content attribute [@stumpylog](https://github.com/stumpylog) ([#3695](https://github.com/paperless-ngx/paperless-ngx/pull/3695))
|
||||
- Fix: Generation of thumbnails for existing stored emails [@stumpylog](https://github.com/stumpylog) ([#3696](https://github.com/paperless-ngx/paperless-ngx/pull/3696))
|
||||
- Fix: Use row gap for filter editor [@kleinweby](https://github.com/kleinweby) ([#3662](https://github.com/paperless-ngx/paperless-ngx/pull/3662))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Documentation: update API docs re permissions [@shamoon](https://github.com/shamoon) ([#3697](https://github.com/paperless-ngx/paperless-ngx/pull/3697))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Chore: Updates codecov configuration for the flag settings and notification delay [@stumpylog](https://github.com/stumpylog) ([#3656](https://github.com/paperless-ngx/paperless-ngx/pull/3656))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>4 changes</summary>
|
||||
|
||||
- Fix: prevent button wrapping when sidebar narrows in MS Edge [@shamoon](https://github.com/shamoon) ([#3682](https://github.com/paperless-ngx/paperless-ngx/pull/3682))
|
||||
- Fix: Handling for filenames with non-ascii and no content attribute [@stumpylog](https://github.com/stumpylog) ([#3695](https://github.com/paperless-ngx/paperless-ngx/pull/3695))
|
||||
- Fix: Generation of thumbnails for existing stored emails [@stumpylog](https://github.com/stumpylog) ([#3696](https://github.com/paperless-ngx/paperless-ngx/pull/3696))
|
||||
- Fix: Use row gap for filter editor [@kleinweby](https://github.com/kleinweby) ([#3662](https://github.com/paperless-ngx/paperless-ngx/pull/3662))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 1.16.3
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: Set user and home environment through supervisord [@stumpylog](https://github.com/stumpylog) ([#3638](https://github.com/paperless-ngx/paperless-ngx/pull/3638))
|
||||
- Fix: Ignore errors when trying to copy the original file's stats [@stumpylog](https://github.com/stumpylog) ([#3652](https://github.com/paperless-ngx/paperless-ngx/pull/3652))
|
||||
- Fix: Copy default thumbnail if thumbnail generation fails [@plu](https://github.com/plu) ([#3632](https://github.com/paperless-ngx/paperless-ngx/pull/3632))
|
||||
- Fix: Set user and home environment through supervisord [@stumpylog](https://github.com/stumpylog) ([#3638](https://github.com/paperless-ngx/paperless-ngx/pull/3638))
|
||||
- Fix: Fix quick install with external database not being fully ready [@stumpylog](https://github.com/stumpylog) ([#3637](https://github.com/paperless-ngx/paperless-ngx/pull/3637))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Chore: Update default Postgres version for new installs [@stumpylog](https://github.com/stumpylog) ([#3640](https://github.com/paperless-ngx/paperless-ngx/pull/3640))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>2 changes</summary>
|
||||
|
||||
- Fix: Ignore errors when trying to copy the original file's stats [@stumpylog](https://github.com/stumpylog) ([#3652](https://github.com/paperless-ngx/paperless-ngx/pull/3652))
|
||||
- Fix: Copy default thumbnail if thumbnail generation fails [@plu](https://github.com/plu) ([#3632](https://github.com/paperless-ngx/paperless-ngx/pull/3632))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 1.16.2
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: Increase httpx operation timeouts to 30s [@stumpylog](https://github.com/stumpylog) ([#3627](https://github.com/paperless-ngx/paperless-ngx/pull/3627))
|
||||
- Fix: Better error handling and checking when parsing documents via Tika [@stumpylog](https://github.com/stumpylog) ([#3617](https://github.com/paperless-ngx/paperless-ngx/pull/3617))
|
||||
|
||||
### Development
|
||||
|
||||
- Development: frontend unit testing [@shamoon](https://github.com/shamoon) ([#3597](https://github.com/paperless-ngx/paperless-ngx/pull/3597))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Chore: Bumps the CI/Docker pipenv version [@stumpylog](https://github.com/stumpylog) ([#3622](https://github.com/paperless-ngx/paperless-ngx/pull/3622))
|
||||
- Chore: Set CI artifact retention days [@stumpylog](https://github.com/stumpylog) ([#3621](https://github.com/paperless-ngx/paperless-ngx/pull/3621))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>3 changes</summary>
|
||||
|
||||
- Fix: Increase httpx operation timeouts to 30s [@stumpylog](https://github.com/stumpylog) ([#3627](https://github.com/paperless-ngx/paperless-ngx/pull/3627))
|
||||
- Fix: Better error handling and checking when parsing documents via Tika [@stumpylog](https://github.com/stumpylog) ([#3617](https://github.com/paperless-ngx/paperless-ngx/pull/3617))
|
||||
- Development: frontend unit testing [@shamoon](https://github.com/shamoon) ([#3597](https://github.com/paperless-ngx/paperless-ngx/pull/3597))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 1.16.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: PIL ImportError on ARM devices with Docker [@stumpylog](https://github.com/stumpylog) ([#3605](https://github.com/paperless-ngx/paperless-ngx/pull/3605))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Chore: Enable the image cleanup action [@stumpylog](https://github.com/stumpylog) ([#3606](https://github.com/paperless-ngx/paperless-ngx/pull/3606))
|
||||
|
||||
## paperless-ngx 1.16.0
|
||||
|
||||
### Notable Changes
|
||||
|
||||
- Chore: Update base image to Debian bookworm [@stumpylog](https://github.com/stumpylog) ([#3469](https://github.com/paperless-ngx/paperless-ngx/pull/3469))
|
||||
|
||||
### Features
|
||||
|
||||
- Feature: Update to a simpler Tika library [@stumpylog](https://github.com/stumpylog) ([#3517](https://github.com/paperless-ngx/paperless-ngx/pull/3517))
|
||||
- Feature: Allow to filter documents by original filename and checksum [@jayme-github](https://github.com/jayme-github) ([#3485](https://github.com/paperless-ngx/paperless-ngx/pull/3485))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: return user first / last name from backend [@shamoon](https://github.com/shamoon) ([#3579](https://github.com/paperless-ngx/paperless-ngx/pull/3579))
|
||||
- Fix use of `PAPERLESS_DB_TIMEOUT` for all db types [@shamoon](https://github.com/shamoon) ([#3576](https://github.com/paperless-ngx/paperless-ngx/pull/3576))
|
||||
- Fix: handle mail rules with no filters on some imap servers [@shamoon](https://github.com/shamoon) ([#3554](https://github.com/paperless-ngx/paperless-ngx/pull/3554))
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Chore: Python dependency updates (celery 5.3.0 in particular) [@stumpylog](https://github.com/stumpylog) ([#3584](https://github.com/paperless-ngx/paperless-ngx/pull/3584))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>8 changes</summary>
|
||||
|
||||
- Chore: Python dependency updates (celery 5.3.0 in particular) [@stumpylog](https://github.com/stumpylog) ([#3584](https://github.com/paperless-ngx/paperless-ngx/pull/3584))
|
||||
- Fix: return user first / last name from backend [@shamoon](https://github.com/shamoon) ([#3579](https://github.com/paperless-ngx/paperless-ngx/pull/3579))
|
||||
- Fix use of `PAPERLESS_DB_TIMEOUT` for all db types [@shamoon](https://github.com/shamoon) ([#3576](https://github.com/paperless-ngx/paperless-ngx/pull/3576))
|
||||
- Fix: handle mail rules with no filters on some imap servers [@shamoon](https://github.com/shamoon) ([#3554](https://github.com/paperless-ngx/paperless-ngx/pull/3554))
|
||||
- Chore: Copy file stats from original file [@stumpylog](https://github.com/stumpylog) ([#3551](https://github.com/paperless-ngx/paperless-ngx/pull/3551))
|
||||
- Chore: Adds test for barcode ASN when it already exists [@stumpylog](https://github.com/stumpylog) ([#3550](https://github.com/paperless-ngx/paperless-ngx/pull/3550))
|
||||
- Feature: Update to a simpler Tika library [@stumpylog](https://github.com/stumpylog) ([#3517](https://github.com/paperless-ngx/paperless-ngx/pull/3517))
|
||||
- Feature: Allow to filter documents by original filename and checksum [@jayme-github](https://github.com/jayme-github) ([#3485](https://github.com/paperless-ngx/paperless-ngx/pull/3485))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 1.15.1
|
||||
|
||||
### Bug Fixes
|
||||
|
@@ -35,6 +35,12 @@ matcher.
|
||||
|
||||
Defaults to `redis://localhost:6379`.
|
||||
|
||||
`PAPERLESS_REDIS_PREFIX=<prefix>`
|
||||
|
||||
: Prefix to be used in Redis for keys and channels. Useful for sharing one Redis server among multiple Paperless instances.
|
||||
|
||||
Defaults to no prefix.
|
||||
|
||||
### Database
|
||||
|
||||
`PAPERLESS_DBENGINE=<engine_name>`
|
||||
@@ -495,6 +501,19 @@ HTTP header/value expected by Django, eg `'["HTTP_X_FORWARDED_PROTO", "https"]'`
|
||||
Settings this value has security implications. Read the Django documentation
|
||||
and be sure you understand its usage before setting it.
|
||||
|
||||
`PAPERLESS_EMAIL_CERTIFICATE_FILE=<path>`
|
||||
|
||||
: Configures an additional SSL certificate file containing a [certificate](https://docs.python.org/3/library/ssl.html#certificates)
|
||||
or certificate chain which should be trusted for validating SSL connections against mail providers.
|
||||
This is for use with self-signed certificates against local IMAP servers.
|
||||
|
||||
Defaults to None.
|
||||
|
||||
!!! warning
|
||||
|
||||
Settings this value has security implications for the security of your email.
|
||||
Understand what it does and be sure you need to before setting.
|
||||
|
||||
## OCR settings {#ocr}
|
||||
|
||||
Paperless uses [OCRmyPDF](https://ocrmypdf.readthedocs.io/en/latest/)
|
||||
@@ -524,7 +543,7 @@ parsing documents.
|
||||
|
||||
`PAPERLESS_OCR_MODE=<mode>`
|
||||
|
||||
: Tell paperless when and how to perform ocr on your documents. Four
|
||||
: Tell paperless when and how to perform ocr on your documents. Three
|
||||
modes are available:
|
||||
|
||||
- `skip`: Paperless skips all pages and will perform ocr only on
|
||||
@@ -1095,6 +1114,64 @@ barcode.
|
||||
|
||||
Defaults to "ASN"
|
||||
|
||||
`PAPERLESS_CONSUMER_BARCODE_UPSCALE=<float>`
|
||||
|
||||
: Defines the upscale factor used in barcode detection.
|
||||
Improves the detection of small barcodes, i.e. with a value of 1.5 by
|
||||
upscaling the document beforce the detection process. Upscaling will
|
||||
only take place if value is bigger than 1.0. Otherwise upscaling will
|
||||
not be performed to save resources. Try using in combination with
|
||||
PAPERLESS_CONSUMER_BARCODE_DPI set to a value higher than default.
|
||||
|
||||
Defaults to 0.0
|
||||
|
||||
`PAPERLESS_CONSUMER_BARCODE_DPI=<int>`
|
||||
|
||||
: During barcode detection every page from a PDF document needs
|
||||
to be converted to an image. A dpi value can be specified in the
|
||||
conversion process. Default is 300. If the detection of small barcodes
|
||||
fails a bigger dpi value i.e. 600 can fix the issue. Try using in
|
||||
combination with PAPERLESS_CONSUMER_BARCODE_UPSCALE bigger than 1.0.
|
||||
|
||||
Defaults to "300"
|
||||
|
||||
## Collate Double-Sided Documents {#collate}
|
||||
|
||||
`PAPERLESS_CONSUMER_ENABLE_COLLATE_DOUBLE_SIDED=<bool>`
|
||||
|
||||
: Enables automatic collation of two single-sided scans into a double-sided
|
||||
document.
|
||||
|
||||
This is useful if you have an automatic document feeder that only supports
|
||||
single-sided scans, but you need to scan a double-sided document. If your
|
||||
ADF supports double-sided scans natively, you do not need this feature.
|
||||
|
||||
`PAPERLESS_CONSUMER_RECURSIVE` must be enabled for this to work.
|
||||
|
||||
For more information, read the [corresponding section in the advanced
|
||||
documentation](/advanced_usage#collate).
|
||||
|
||||
Defaults to false.
|
||||
|
||||
`PAPERLESS_CONSUMER_COLLATE_DOUBLE_SIDED_SUBDIR_NAME=<str>`
|
||||
|
||||
: The name of the subdirectory that the collate feature expects documents to
|
||||
arrive.
|
||||
|
||||
This only has an effect if `PAPERLESS_CONSUMER_ENABLE_COLLATE_DOUBLE_SIDED`
|
||||
has been enabled. Note that Paperless will not automatically create the
|
||||
directory.
|
||||
|
||||
Defaults to "double-sided".
|
||||
|
||||
`PAPERLESS_CONSUMER_COLLATE_DOUBLE_SIDED_TIFF_SUPPORT=<bool>`
|
||||
: Whether TIFF image files should be supported when collating documents.
|
||||
This will automatically convert any TIFF image(s) to pdfs for later
|
||||
processing. This only has an effect if
|
||||
`PAPERLESS_CONSUMER_ENABLE_COLLATE_DOUBLE_SIDED` has been enabled.
|
||||
|
||||
Defaults to false.
|
||||
|
||||
## Binaries
|
||||
|
||||
There are a few external software packages that Paperless expects to
|
||||
@@ -1102,7 +1179,7 @@ find on your system when it starts up. Unless you've done something
|
||||
creative with their installation, you probably won't need to edit any
|
||||
of these. However, if you've installed these programs somewhere where
|
||||
simply typing the name of the program doesn't automatically execute it
|
||||
(ie. the program isn't in your \$PATH), then you'll need to specify
|
||||
(ie. the program isn't in your $PATH), then you'll need to specify
|
||||
the literal path for that program.
|
||||
|
||||
`PAPERLESS_CONVERT_BINARY=<path>`
|
||||
@@ -1186,7 +1263,7 @@ actual group ID on the host system, which you can get by executing
|
||||
with English, German, Italian, Spanish and French. If your language
|
||||
is not in this list, install additional languages with this
|
||||
configuration option. You will need to [find the right LangCodes](https://tesseract-ocr.github.io/tessdoc/Data-Files-in-different-versions.html)
|
||||
but note that (tesseract-ocr-\* package names)[https://packages.debian.org/bullseye/graphics/]
|
||||
but note that [tesseract-ocr-\* package names](https://packages.debian.org/bullseye/graphics/)
|
||||
do not always correspond with the language codes e.g. "chi_tra" should be
|
||||
specified as "chi-tra".
|
||||
|
||||
|
@@ -58,7 +58,7 @@ first-time setup.
|
||||
|
||||
!!! note
|
||||
|
||||
Every command is executed directly from the root folder of the project unless specified otherwise.
|
||||
Every command is executed directly from the root folder of the project unless specified otherwise.
|
||||
|
||||
1. Install prerequisites + pipenv as mentioned in
|
||||
[Bare metal route](/setup#bare_metal).
|
||||
@@ -177,69 +177,69 @@ The front end is built using AngularJS. In order to get started, you need Node.j
|
||||
|
||||
The following commands are all performed in the `src-ui`-directory. You will need a running back end (including an active session) to connect to the back end API. To spin it up refer to the commands under the section [above](#back-end-development).
|
||||
|
||||
1. Install the Angular CLI. You might need sudo privileges
|
||||
to perform this command:
|
||||
1. Install the Angular CLI. You might need sudo privileges to perform this command:
|
||||
|
||||
```bash
|
||||
$ npm install -g @angular/cli
|
||||
```
|
||||
```bash
|
||||
$ npm install -g @angular/cli
|
||||
```
|
||||
|
||||
2. Make sure that it's on your path.
|
||||
2. Make sure that it's on your path.
|
||||
|
||||
3. Install all necessary modules:
|
||||
3. Install all necessary modules:
|
||||
|
||||
```bash
|
||||
$ npm install
|
||||
```
|
||||
```bash
|
||||
$ npm install
|
||||
```
|
||||
|
||||
4. You can launch a development server by running:
|
||||
4. You can launch a development server by running:
|
||||
|
||||
```bash
|
||||
$ ng serve
|
||||
```
|
||||
```bash
|
||||
$ ng serve
|
||||
```
|
||||
|
||||
This will automatically update whenever you save. However, in-place
|
||||
compilation might fail on syntax errors, in which case you need to
|
||||
restart it.
|
||||
This will automatically update whenever you save. However, in-place
|
||||
compilation might fail on syntax errors, in which case you need to
|
||||
restart it.
|
||||
|
||||
By default, the development server is available on `http://localhost:4200/` and is configured to access the API at
|
||||
`http://localhost:8000/api/`, which is the default of the backend. If you enabled `DEBUG` on the back end, several security overrides for allowed hosts, CORS and X-Frame-Options are in place so that the front end behaves exactly as in production.
|
||||
By default, the development server is available on `http://localhost:4200/` and is configured to access the API at
|
||||
`http://localhost:8000/api/`, which is the default of the backend. If you enabled `DEBUG` on the back end, several security overrides for allowed hosts, CORS and X-Frame-Options are in place so that the front end behaves exactly as in production.
|
||||
|
||||
### Testing and code style
|
||||
|
||||
- The front end code (.ts, .html, .scss) use `prettier` for code
|
||||
formatting via the Git `pre-commit` hooks which run automatically on
|
||||
commit. See [above](#code-formatting-with-pre-commit-hooks) for installation instructions. You can also run this via the CLI with a
|
||||
command such as
|
||||
The front end code (.ts, .html, .scss) use `prettier` for code
|
||||
formatting via the Git `pre-commit` hooks which run automatically on
|
||||
commit. See [above](#code-formatting-with-pre-commit-hooks) for installation instructions. You can also run this via the CLI with a
|
||||
command such as
|
||||
|
||||
```bash
|
||||
$ git ls-files -- '*.ts' | xargs pre-commit run prettier --files
|
||||
```
|
||||
```bash
|
||||
$ git ls-files -- '*.ts' | xargs pre-commit run prettier --files
|
||||
```
|
||||
|
||||
- Front end testing uses jest and cypress. There is currently a need
|
||||
for significantly more front end tests. Unit tests and e2e tests,
|
||||
respectively, can be run non-interactively with:
|
||||
Front end testing uses Jest and Playwright. Unit tests and e2e tests,
|
||||
respectively, can be run non-interactively with:
|
||||
|
||||
```bash
|
||||
$ ng test
|
||||
$ npm run e2e:ci
|
||||
```
|
||||
```bash
|
||||
$ ng test
|
||||
$ npx playwright test
|
||||
```
|
||||
|
||||
- Cypress also includes a UI which can be run with:
|
||||
Playwright also includes a UI which can be run with:
|
||||
|
||||
```bash
|
||||
$ ./node_modules/.bin/cypress open
|
||||
```
|
||||
```bash
|
||||
$ npx playwright test --ui
|
||||
```
|
||||
|
||||
- In order to build the front end and serve it as part of Django, execute:
|
||||
### Building the frontend
|
||||
|
||||
```bash
|
||||
$ ng build --configuration production
|
||||
```
|
||||
In order to build the front end and serve it as part of Django, execute:
|
||||
|
||||
This will build the front end and put it in a location from which the
|
||||
Django server will serve it as static content. This way, you can verify
|
||||
that authentication is working.
|
||||
```bash
|
||||
$ ng build --configuration production
|
||||
```
|
||||
|
||||
This will build the front end and put it in a location from which the
|
||||
Django server will serve it as static content. This way, you can verify
|
||||
that authentication is working.
|
||||
|
||||
## Localization
|
||||
|
||||
|
13
docs/faq.md
13
docs/faq.md
@@ -3,10 +3,11 @@
|
||||
## _What's the general plan for Paperless-ngx?_
|
||||
|
||||
**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
|
||||
"feature-complete", it is a community-driven project and development
|
||||
will be guided in this way. New features can be submitted via
|
||||
[GitHub discussions](https://github.com/paperless-ngx/paperless-ngx/discussions)
|
||||
and "up-voted" by the community, but this is not a
|
||||
guarantee that the feature will be implemented. This project will always be
|
||||
open to collaboration in the form of PRs, ideas etc.
|
||||
|
||||
## _I'm using docker. Where are my documents?_
|
||||
@@ -58,7 +59,7 @@ elsewhere. Here are a couple notes about that.
|
||||
WebP images are processed with OCR and converted into PDF documents.
|
||||
- Plain text documents are supported as well and are added verbatim to
|
||||
paperless.
|
||||
- With the optional Tika integration enabled (see [Tika configuration](/configuration#tika),
|
||||
- With the optional Tika integration enabled (see [Tika configuration](https://docs.paperless-ngx.com/configuration#tika)),
|
||||
Paperless also supports various Office documents (.docx, .doc, odt,
|
||||
.ppt, .pptx, .odp, .xls, .xlsx, .ods).
|
||||
|
||||
@@ -82,7 +83,7 @@ has to do much less work to serve the data.
|
||||
## _How do I install paperless-ngx on Raspberry Pi?_
|
||||
|
||||
**A:** Docker images are available for armv7 and arm64 hardware, so just
|
||||
follow the docker-compose instructions. Apart from more required disk
|
||||
follow the [docker-compose instructions](https://docs.paperless-ngx.com/setup/#installation). Apart from more required disk
|
||||
space compared to a bare metal installation, docker comes with close to
|
||||
zero overhead, even on Raspberry Pi.
|
||||
|
||||
|
@@ -259,6 +259,7 @@ supported.
|
||||
- `python3-pip`
|
||||
- `python3-dev`
|
||||
- `default-libmysqlclient-dev` for MariaDB
|
||||
- `pkg-config` for mysqlclient (python dependency)
|
||||
- `fonts-liberation` for generating thumbnails for plain text
|
||||
files
|
||||
- `imagemagick` >= 6 for PDF conversion
|
||||
@@ -273,7 +274,7 @@ supported.
|
||||
Use this list for your preferred package management:
|
||||
|
||||
```
|
||||
python3 python3-pip python3-dev imagemagick fonts-liberation gnupg libpq-dev default-libmysqlclient-dev libmagic-dev mime-support libzbar0 poppler-utils
|
||||
python3 python3-pip python3-dev imagemagick fonts-liberation gnupg libpq-dev default-libmysqlclient-dev pkg-config libmagic-dev mime-support libzbar0 poppler-utils
|
||||
```
|
||||
|
||||
These dependencies are required for OCRmyPDF, which is used for text
|
||||
|
@@ -221,9 +221,18 @@ As of version 1.14.0 Paperless-ngx added core support for user / group permissio
|
||||
based around 'global' permissions as well as 'object-level' permissions. Global permissions designate
|
||||
which parts of the application a user can access (e.g. Documents, Tags, Settings) and object-level
|
||||
determine which objects are visible or editable. All objects have an 'owner' and 'view' and 'edit'
|
||||
permissions which can be granted to other users or groups.
|
||||
permissions which can be granted to other users or groups. The paperless-ngx permissions system uses
|
||||
the built-in user model of the backend framework, Django.
|
||||
|
||||
Permissions uses the built-in user model of the backend framework, Django.
|
||||
!!! tip
|
||||
|
||||
Object-level permissions only apply to the object itself. In other words, setting permissions
|
||||
for a Tag will _not_ affect the permissions of documents that have the Tag.
|
||||
|
||||
Permissions can be set using the new "Permissions" tab when editing documents, or bulk-applied
|
||||
in the UI by selecting documents and choosing the "Permissions" button. Owner can also optionally
|
||||
be set for documents uploaded via the API. Documents consumed via the consumption dir currently
|
||||
do not have an owner set.
|
||||
|
||||
!!! note
|
||||
|
||||
@@ -238,11 +247,6 @@ Permissions uses the built-in user model of the backend framework, Django.
|
||||
|
||||
Note that superusers have access to all objects.
|
||||
|
||||
Permissions can be set using the new "Permissions" tab when editing documents, or bulk-applied
|
||||
in the UI by selecting documents and choosing the "Permissions" button. Owner can also optionally
|
||||
be set for documents uploaded via the API. Documents consumed via the consumption dir currently
|
||||
do not have an owner set.
|
||||
|
||||
### Users and Groups
|
||||
|
||||
Paperless-ngx versions after 1.14.0 allow creating and editing users and groups via the 'frontend' UI.
|
||||
|
@@ -72,7 +72,7 @@ fi
|
||||
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."
|
||||
echo "WARN: Use 'sudo usermod -aG docker $USER' to assign Docker permissions to the user (may require restarting shell)."
|
||||
echo ""
|
||||
sleep 3
|
||||
fi
|
||||
@@ -384,6 +384,14 @@ fi
|
||||
|
||||
${DOCKER_COMPOSE_CMD} pull
|
||||
|
||||
if [ "$DATABASE_BACKEND" == "postgres" ] || [ "$DATABASE_BACKEND" == "mariadb" ] ; then
|
||||
echo "Starting DB first for initilzation"
|
||||
${DOCKER_COMPOSE_CMD} up --detach db
|
||||
# hopefully enough time for even the slower systems
|
||||
sleep 15
|
||||
${DOCKER_COMPOSE_CMD} stop
|
||||
fi
|
||||
|
||||
${DOCKER_COMPOSE_CMD} run --rm -e DJANGO_SUPERUSER_PASSWORD="$PASSWORD" webserver createsuperuser --noinput --username "$USERNAME" --email "$EMAIL"
|
||||
|
||||
${DOCKER_COMPOSE_CMD} up --detach
|
||||
|
@@ -66,6 +66,11 @@
|
||||
#PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=false
|
||||
#PAPERLESS_CONSUMER_ENABLE_BARCODES=false
|
||||
#PAPERLESS_CONSUMER_BARCODE_STRING=PATCHT
|
||||
#PAPERLESS_CONSUMER_BARCODE_UPSCALE=0.0
|
||||
#PAPERLESS_CONSUMER_BARCODE_DPI=300
|
||||
#PAPERLESS_CONSUMER_ENABLE_COLLATE_DOUBLE_SIDED=false
|
||||
#PAPERLESS_CONSUMER_COLLATE_DOUBLE_SIDED_SUBDIR_NAME=double-sided
|
||||
#PAPERLESS_CONSUMER_COLLATE_DOUBLE_SIDED_TIFF_SUPPORT=false
|
||||
#PAPERLESS_PRE_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh
|
||||
#PAPERLESS_POST_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh
|
||||
#PAPERLESS_FILENAME_DATE_ORDER=YMD
|
||||
|
@@ -1,6 +1,6 @@
|
||||
#!/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 -p 5432:5432 -e POSTGRES_PASSWORD=password -v paperless_pgdata:/var/lib/postgresql/data -d postgres:15
|
||||
docker run -d -p 6379:6379 redis:latest
|
||||
docker run -p 3000:3000 -d gotenberg/gotenberg:7.8 gotenberg --chromium-disable-javascript=true --chromium-allow-list="file:///tmp/.*"
|
||||
docker run -p 9998:9998 -d ghcr.io/paperless-ngx/tika:latest
|
||||
|
@@ -34,10 +34,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",
|
||||
"sk-SK": "src/locale/messages.sk_SK.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",
|
||||
"uk-UA": "src/locale/messages.uk_UA.xlf",
|
||||
"zh-CN": "src/locale/messages.zh_CN.xlf"
|
||||
}
|
||||
},
|
||||
@@ -159,7 +161,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "paperless-ui",
|
||||
"cli": {
|
||||
"schematicCollections": [
|
||||
"@angular-eslint/schematics"
|
||||
|
@@ -12,9 +12,13 @@ test('should activate / deactivate save button when changes are saved', async ({
|
||||
await expect(page.getByTitle('Storage path', { exact: true })).toHaveText(
|
||||
/\w+/
|
||||
)
|
||||
await expect(page.getByRole('button', { name: 'Save' })).toBeDisabled()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Save', exact: true })
|
||||
).toBeDisabled()
|
||||
await page.getByTitle('Storage path').getByTitle('Clear all').click()
|
||||
await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Save', exact: true })
|
||||
).toBeEnabled()
|
||||
})
|
||||
|
||||
test('should warn on unsaved changes', async ({ page }) => {
|
||||
@@ -23,13 +27,17 @@ test('should warn on unsaved changes', async ({ page }) => {
|
||||
await expect(page.getByTitle('Correspondent', { exact: true })).toHaveText(
|
||||
/\w+/
|
||||
)
|
||||
await expect(page.getByRole('button', { name: 'Save' })).toBeDisabled()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Save', exact: true })
|
||||
).toBeDisabled()
|
||||
await page
|
||||
.getByTitle('Storage path', { exact: true })
|
||||
.getByTitle('Clear all')
|
||||
.click()
|
||||
await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled()
|
||||
await page.getByRole('button', { name: 'Close' }).click()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Save', exact: true })
|
||||
).toBeEnabled()
|
||||
await page.getByRole('button', { name: 'Close', exact: true }).click()
|
||||
await expect(page.getByRole('dialog')).toHaveText(/unsaved changes/)
|
||||
await page.getByRole('button', { name: 'Cancel' }).click()
|
||||
await page.getByRole('link', { name: 'Close all' }).click()
|
||||
@@ -86,51 +94,6 @@ test('should show a list of notes', async ({ page }) => {
|
||||
).toHaveCount(4)
|
||||
})
|
||||
|
||||
test('should support note deletion', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||
await page.goto('/documents/175/notes')
|
||||
await expect(page.locator('app-document-notes')).toBeVisible()
|
||||
const deletePromise = page.waitForRequest(
|
||||
(request) =>
|
||||
request.method() === 'DELETE' &&
|
||||
request.url().includes('/api/documents/175/notes/')
|
||||
)
|
||||
await page
|
||||
.getByRole('button', { name: /delete note/i, includeHidden: true })
|
||||
.first()
|
||||
.click()
|
||||
await deletePromise
|
||||
})
|
||||
|
||||
test('should support note insertion', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||
await page.goto('/documents/175/notes')
|
||||
await expect(page.locator('app-document-notes')).toBeVisible()
|
||||
await expect(
|
||||
await page.getByRole('button', {
|
||||
name: /delete note/i,
|
||||
includeHidden: true,
|
||||
})
|
||||
).toHaveCount(4)
|
||||
await page.getByPlaceholder('Enter note').fill('This is a new note')
|
||||
const addPromise = page.waitForRequest((request) => {
|
||||
if (!request.url().includes('/notes/')) {
|
||||
// ignore other requests
|
||||
return true
|
||||
} else {
|
||||
const data = request.postDataJSON()
|
||||
const isValid = data['note'] === 'This is a new note'
|
||||
return (
|
||||
isValid &&
|
||||
request.method() === 'POST' &&
|
||||
request.url().includes('/notes/')
|
||||
)
|
||||
}
|
||||
})
|
||||
await page.getByRole('button', { name: 'Add note' }).click()
|
||||
await addPromise
|
||||
})
|
||||
|
||||
test('should support quick filters', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR2, { notFound: 'fallback' })
|
||||
await page.goto('/documents/175/details')
|
||||
|
@@ -1,58 +0,0 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
const REQUESTS_HAR1 = 'e2e/manage/requests/api-manage1.har'
|
||||
const REQUESTS_HAR2 = 'e2e/manage/requests/api-manage2.har'
|
||||
|
||||
test('should show a list of tags with bottom pagination as well', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
|
||||
await page.goto('/tags')
|
||||
await expect(page.getByRole('main')).toHaveText(/26 total tags/i)
|
||||
await expect(await page.locator('ngb-pagination')).toHaveCount(2)
|
||||
})
|
||||
|
||||
test('should show a list of correspondents without bottom pagination', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR2, { notFound: 'fallback' })
|
||||
await page.goto('/correspondents')
|
||||
await expect(page.getByRole('main')).toHaveText(/4 total correspondents/i)
|
||||
await expect(await page.locator('ngb-pagination')).toHaveCount(1)
|
||||
})
|
||||
|
||||
test('should support quick filter Documents button', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
|
||||
await page.goto('/tags')
|
||||
await page
|
||||
.getByRole('row', { name: 'Inbox' })
|
||||
.getByRole('button', { name: 'Documents' })
|
||||
.click()
|
||||
await expect(page).toHaveURL(/tags__id__all=9/)
|
||||
})
|
||||
|
||||
test('should support item editing', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
|
||||
await page.goto('/tags')
|
||||
await page
|
||||
.getByRole('row', { name: 'Inbox' })
|
||||
.getByRole('button', { name: 'Edit' })
|
||||
.click()
|
||||
await expect(page.getByRole('dialog')).toBeVisible()
|
||||
await expect(page.getByLabel('Name')).toHaveValue('Inbox')
|
||||
await page.getByTitle('Color').getByRole('button').click()
|
||||
const color = await page.getByLabel('Color').inputValue()
|
||||
|
||||
const updatePromise = page.waitForRequest((request) => {
|
||||
const data = request.postDataJSON()
|
||||
const isValid = data['color'] === color
|
||||
return (
|
||||
isValid &&
|
||||
request.method() === 'PUT' &&
|
||||
request.url().includes('/api/tags/9/')
|
||||
)
|
||||
})
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click()
|
||||
await updatePromise
|
||||
})
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,8 +1,6 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
const REQUESTS_HAR = 'e2e/settings/requests/api-settings.har'
|
||||
const REQUESTS_HAR2 = 'e2e/settings/requests/api-settings2.har'
|
||||
const REQUESTS_HAR3 = 'e2e/settings/requests/api-settings3.har'
|
||||
|
||||
test('should post settings on save', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||
@@ -101,65 +99,3 @@ test('should support tab direct navigation', async ({ page }) => {
|
||||
page.getByRole('tab', { name: 'Users & Groups' })
|
||||
).toHaveAttribute('aria-selected', 'true')
|
||||
})
|
||||
|
||||
test('should show a list of mail accounts & support creation', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR2, { notFound: 'fallback' })
|
||||
await page.goto('/settings/mail')
|
||||
await expect(
|
||||
page.getByRole('listitem').filter({ hasText: 'imap.gmail.com' })
|
||||
).toHaveCount(1)
|
||||
await expect(
|
||||
page.getByRole('listitem').filter({ hasText: 'imap.domain.com' })
|
||||
).toHaveCount(1)
|
||||
await page.getByRole('button', { name: /Add Account/ }).click()
|
||||
await expect(page.getByRole('dialog')).toHaveCount(1)
|
||||
await page.getByLabel('Name', { exact: true }).fill('Test Account')
|
||||
await page.getByLabel('IMAP Server', { exact: true }).fill('imap.server.com')
|
||||
await page.getByLabel('IMAP Port', { exact: true }).fill('993')
|
||||
await page.getByLabel('Username', { exact: true }).fill('username')
|
||||
await page.getByLabel('Password', { exact: true }).fill('password')
|
||||
const createPromise = page.waitForRequest((request) => {
|
||||
const data = request.postDataJSON()
|
||||
const isValid = data['imap_server'] === 'imap.server.com'
|
||||
return (
|
||||
isValid &&
|
||||
request.method() === 'POST' &&
|
||||
request.url().includes('/api/mail_accounts/')
|
||||
)
|
||||
})
|
||||
await page.getByRole('button', { name: 'Save' }).click()
|
||||
await createPromise
|
||||
})
|
||||
|
||||
test('should show a list of mail rules & support creation', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR3, { notFound: 'fallback' })
|
||||
await page.goto('/settings/mail')
|
||||
await expect(
|
||||
page.getByRole('listitem').filter({ hasText: 'domain' })
|
||||
).toHaveCount(2)
|
||||
await expect(
|
||||
page.getByRole('listitem').filter({ hasText: 'gmail' })
|
||||
).toHaveCount(2)
|
||||
await page.getByRole('button', { name: /Add Rule/ }).click()
|
||||
await expect(page.getByRole('dialog')).toHaveCount(1)
|
||||
await page.getByLabel('Name', { exact: true }).fill('Test Rule')
|
||||
await page.getByTitle('Account').locator('span').first().click()
|
||||
await page.getByRole('option', { name: 'gmail' }).click()
|
||||
await page.getByLabel('Maximum age (days)').fill('0')
|
||||
const createPromise = page.waitForRequest((request) => {
|
||||
const data = request.postDataJSON()
|
||||
const isValid = data['name'] === 'Test Rule'
|
||||
return (
|
||||
isValid &&
|
||||
request.method() === 'POST' &&
|
||||
request.url().includes('/api/mail_rules/')
|
||||
)
|
||||
})
|
||||
await page.getByRole('button', { name: 'Save' }).scrollIntoViewIfNeeded()
|
||||
await page.getByRole('button', { name: 'Save' }).click()
|
||||
await createPromise
|
||||
})
|
||||
|
File diff suppressed because one or more lines are too long
@@ -1,71 +0,0 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
const REQUESTS_HAR = 'e2e/tasks/requests/api-tasks.har'
|
||||
|
||||
test('should show a list of dismissable tasks in tabs', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||
await page.goto('/tasks')
|
||||
await expect(page.getByRole('tab', { name: /Failed/ })).toHaveText(/1/)
|
||||
await expect(
|
||||
page.getByRole('cell').filter({ hasText: 'Dismiss' })
|
||||
).toHaveCount(1)
|
||||
await expect(page.getByRole('tab', { name: /Complete/ })).toHaveText(/8/)
|
||||
await page.getByRole('tab', { name: /Complete/ }).click()
|
||||
await expect(
|
||||
page.getByRole('cell').filter({ hasText: 'Dismiss' })
|
||||
).toHaveCount(8)
|
||||
await page.getByRole('tab', { name: /Started/ }).click()
|
||||
await expect(
|
||||
page.getByRole('cell').filter({ hasText: 'Dismiss' })
|
||||
).toHaveCount(0)
|
||||
await page.getByRole('tab', { name: /Queued/ }).click()
|
||||
await expect(
|
||||
page.getByRole('cell').filter({ hasText: 'Dismiss' })
|
||||
).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('should support dismissing tasks', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||
await page.goto('/tasks')
|
||||
await page.getByRole('tab', { name: /Failed/ }).click()
|
||||
const dismissPromise = page.waitForRequest((request) => {
|
||||
const data = request.postDataJSON()
|
||||
const isValid = Array.isArray(data['tasks']) && data['tasks'].includes(255)
|
||||
return (
|
||||
isValid &&
|
||||
request.method() === 'POST' &&
|
||||
request.url().includes('/api/acknowledge_tasks/')
|
||||
)
|
||||
})
|
||||
await page
|
||||
.getByRole('button', { name: 'Dismiss', exact: true })
|
||||
.first()
|
||||
.click()
|
||||
await dismissPromise
|
||||
})
|
||||
|
||||
test('should support dismiss all tasks', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||
await page.goto('/tasks')
|
||||
await expect(page.getByRole('button', { name: 'Dismiss all' })).toBeEnabled()
|
||||
await page.getByRole('button', { name: 'Dismiss all' }).click()
|
||||
const dismissPromise = page.waitForRequest((request) => {
|
||||
const data = request.postDataJSON()
|
||||
const isValid = Array.isArray(data['tasks'])
|
||||
return (
|
||||
isValid &&
|
||||
request.method() === 'POST' &&
|
||||
request.url().includes('/api/acknowledge_tasks/')
|
||||
)
|
||||
})
|
||||
await page.getByRole('button', { name: /Dismiss/ }).click()
|
||||
await dismissPromise
|
||||
})
|
||||
|
||||
test('should warn on dismiss all tasks', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||
await page.goto('/tasks')
|
||||
await expect(page.getByRole('button', { name: 'Dismiss all' })).toBeEnabled()
|
||||
await page.getByRole('button', { name: 'Dismiss all' }).click()
|
||||
await expect(page.getByRole('dialog')).toHaveCount(1)
|
||||
})
|
@@ -1,8 +1,14 @@
|
||||
module.exports = {
|
||||
moduleNameMapper: {
|
||||
'@core/(.*)': '<rootDir>/src/app/core/$1',
|
||||
},
|
||||
preset: 'jest-preset-angular',
|
||||
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
|
||||
testPathIgnorePatterns: ['/node_modules/', '/cypress/'],
|
||||
testPathIgnorePatterns: [
|
||||
'/node_modules/',
|
||||
'/e2e/',
|
||||
'abstract-name-filter-service',
|
||||
'abstract-paperless-service',
|
||||
],
|
||||
transformIgnorePatterns: [`<rootDir>/node_modules/(?!.*\\.mjs$|lodash-es)`],
|
||||
moduleNameMapper: {
|
||||
'^src/(.*)': '<rootDir>/src/$1',
|
||||
},
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
8944
src-ui/package-lock.json
generated
8944
src-ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,58 +5,59 @@
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"test": "ng test",
|
||||
"test": "ng test --no-watch --coverage",
|
||||
"lint": "ng lint"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/common": "~15.2.8",
|
||||
"@angular/compiler": "~15.2.8",
|
||||
"@angular/core": "~15.2.8",
|
||||
"@angular/forms": "~15.2.8",
|
||||
"@angular/localize": "~15.2.8",
|
||||
"@angular/platform-browser": "~15.2.8",
|
||||
"@angular/platform-browser-dynamic": "~15.2.8",
|
||||
"@angular/router": "~15.2.8",
|
||||
"@ng-bootstrap/ng-bootstrap": "^14.2.0",
|
||||
"@ng-select/ng-select": "^10.0.4",
|
||||
"@angular/common": "~16.1.7",
|
||||
"@angular/compiler": "~16.1.7",
|
||||
"@angular/core": "~16.1.7",
|
||||
"@angular/forms": "~16.1.7",
|
||||
"@angular/localize": "~16.1.7",
|
||||
"@angular/platform-browser": "~16.1.7",
|
||||
"@angular/platform-browser-dynamic": "~16.1.7",
|
||||
"@angular/router": "~16.1.7",
|
||||
"@ng-bootstrap/ng-bootstrap": "^15.1.0",
|
||||
"@ng-select/ng-select": "^11.1.1",
|
||||
"@ngneat/dirty-check-forms": "^3.0.3",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"bootstrap": "^5.3.0",
|
||||
"bootstrap": "^5.3.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"mime-names": "^1.0.0",
|
||||
"ng2-pdf-viewer": "^9.1.5",
|
||||
"ngx-color": "^8.0.3",
|
||||
"ngx-cookie-service": "^15.0.0",
|
||||
"ngx-file-drop": "^15.0.0",
|
||||
"ngx-ui-tour-ng-bootstrap": "^12.6.0",
|
||||
"ngx-color": "^9.0.0",
|
||||
"ngx-cookie-service": "^16.0.0",
|
||||
"ngx-file-drop": "^16.0.0",
|
||||
"ngx-ui-tour-ng-bootstrap": "^13.0.3",
|
||||
"rxjs": "^7.8.1",
|
||||
"tslib": "^2.5.2",
|
||||
"tslib": "^2.6.1",
|
||||
"uuid": "^9.0.0",
|
||||
"zone.js": "^0.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/jest": "15.0.0",
|
||||
"@angular-devkit/build-angular": "~15.2.6",
|
||||
"@angular-eslint/builder": "15.2.1",
|
||||
"@angular-eslint/eslint-plugin": "15.2.1",
|
||||
"@angular-eslint/eslint-plugin-template": "15.2.1",
|
||||
"@angular-eslint/schematics": "15.2.1",
|
||||
"@angular-eslint/template-parser": "15.2.1",
|
||||
"@angular/cli": "~15.2.7",
|
||||
"@angular/compiler-cli": "~15.2.8",
|
||||
"@playwright/test": "^1.34.3",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/node": "^20.2.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.8",
|
||||
"@typescript-eslint/parser": "^5.59.8",
|
||||
"@angular-builders/jest": "16.0.0",
|
||||
"@angular-devkit/build-angular": "~16.1.6",
|
||||
"@angular-eslint/builder": "16.1.0",
|
||||
"@angular-eslint/eslint-plugin": "16.1.0",
|
||||
"@angular-eslint/eslint-plugin-template": "16.1.0",
|
||||
"@angular-eslint/schematics": "16.1.0",
|
||||
"@angular-eslint/template-parser": "16.1.0",
|
||||
"@angular/cli": "~16.1.6",
|
||||
"@angular/compiler-cli": "~16.1.3",
|
||||
"@playwright/test": "^1.36.2",
|
||||
"@types/jest": "^29.5.3",
|
||||
"@types/node": "^20.4.5",
|
||||
"@typescript-eslint/eslint-plugin": "^6.2.1",
|
||||
"@typescript-eslint/parser": "^6.2.1",
|
||||
"concurrently": "^8.1.0",
|
||||
"eslint": "^8.41.0",
|
||||
"jest": "28.1.3",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"jest-preset-angular": "^12.2.6",
|
||||
"eslint": "^8.46.0",
|
||||
"jest": "29.6.2",
|
||||
"jest-environment-jsdom": "^29.6.2",
|
||||
"jest-preset-angular": "^13.1.1",
|
||||
"jest-websocket-mock": "^2.4.0",
|
||||
"ts-node": "~10.9.1",
|
||||
"typescript": "~4.9.5",
|
||||
"typescript": "^5.1.6",
|
||||
"wait-on": "^7.0.1"
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,63 @@
|
||||
import { jest } from '@jest/globals'
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
require('jest-preset-angular/setup-jest')
|
||||
}
|
||||
import '@angular/localize/init'
|
||||
import { TextEncoder, TextDecoder } from 'util'
|
||||
global.TextEncoder = TextEncoder
|
||||
global.TextDecoder = TextDecoder
|
||||
|
||||
import { registerLocaleData } from '@angular/common'
|
||||
import localeAr from '@angular/common/locales/ar'
|
||||
import localeBe from '@angular/common/locales/be'
|
||||
import localeCa from '@angular/common/locales/ca'
|
||||
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 localeFi from '@angular/common/locales/fi'
|
||||
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 localeSk from '@angular/common/locales/sk'
|
||||
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 localeUk from '@angular/common/locales/uk'
|
||||
import localeZh from '@angular/common/locales/zh'
|
||||
|
||||
registerLocaleData(localeAr)
|
||||
registerLocaleData(localeBe)
|
||||
registerLocaleData(localeCa)
|
||||
registerLocaleData(localeCs)
|
||||
registerLocaleData(localeDa)
|
||||
registerLocaleData(localeDe)
|
||||
registerLocaleData(localeEnGb)
|
||||
registerLocaleData(localeEs)
|
||||
registerLocaleData(localeFi)
|
||||
registerLocaleData(localeFr)
|
||||
registerLocaleData(localeIt)
|
||||
registerLocaleData(localeLb)
|
||||
registerLocaleData(localeNl)
|
||||
registerLocaleData(localePl)
|
||||
registerLocaleData(localePt, 'pt-BR')
|
||||
registerLocaleData(localePt, 'pt-PT')
|
||||
registerLocaleData(localeRo)
|
||||
registerLocaleData(localeRu)
|
||||
registerLocaleData(localeSk)
|
||||
registerLocaleData(localeSl)
|
||||
registerLocaleData(localeSr)
|
||||
registerLocaleData(localeSv)
|
||||
registerLocaleData(localeTr)
|
||||
registerLocaleData(localeUk)
|
||||
registerLocaleData(localeZh)
|
||||
|
||||
/* global mocks for jsdom */
|
||||
const mock = () => {
|
||||
@@ -17,14 +76,7 @@ Object.defineProperty(window, 'getComputedStyle', {
|
||||
value: () => ['-webkit-appearance'],
|
||||
})
|
||||
|
||||
Object.defineProperty(document.body.style, 'transform', {
|
||||
value: () => {
|
||||
return {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
}
|
||||
},
|
||||
})
|
||||
Object.defineProperty(window, 'ResizeObserver', { value: mock() })
|
||||
|
||||
HTMLCanvasElement.prototype.getContext = <
|
||||
typeof HTMLCanvasElement.prototype.getContext
|
||||
|
@@ -22,7 +22,7 @@ import {
|
||||
PermissionType,
|
||||
} from './services/permissions.service'
|
||||
|
||||
const routes: Routes = [
|
||||
export const routes: Routes = [
|
||||
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
|
||||
{
|
||||
path: '',
|
||||
|
182
src-ui/src/app/app.component.spec.ts
Normal file
182
src-ui/src/app/app.component.spec.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
discardPeriodicTasks,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { Router } from '@angular/router'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import { NgxFileDropModule } from 'ngx-file-drop'
|
||||
import { TourService, TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
|
||||
import { Subject } from 'rxjs'
|
||||
import { routes } from './app-routing.module'
|
||||
import { AppComponent } from './app.component'
|
||||
import { ToastsComponent } from './components/common/toasts/toasts.component'
|
||||
import {
|
||||
ConsumerStatusService,
|
||||
FileStatus,
|
||||
} from './services/consumer-status.service'
|
||||
import { PermissionsService } from './services/permissions.service'
|
||||
import { ToastService, Toast } from './services/toast.service'
|
||||
import { UploadDocumentsService } from './services/upload-documents.service'
|
||||
import { SettingsService } from './services/settings.service'
|
||||
|
||||
describe('AppComponent', () => {
|
||||
let component: AppComponent
|
||||
let fixture: ComponentFixture<AppComponent>
|
||||
let tourService: TourService
|
||||
let consumerStatusService: ConsumerStatusService
|
||||
let permissionsService: PermissionsService
|
||||
let toastService: ToastService
|
||||
let router: Router
|
||||
let settingsService: SettingsService
|
||||
let uploadDocumentsService: UploadDocumentsService
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AppComponent, ToastsComponent],
|
||||
providers: [],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
TourNgBootstrapModule,
|
||||
RouterTestingModule.withRoutes(routes),
|
||||
NgxFileDropModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
tourService = TestBed.inject(TourService)
|
||||
consumerStatusService = TestBed.inject(ConsumerStatusService)
|
||||
permissionsService = TestBed.inject(PermissionsService)
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
router = TestBed.inject(Router)
|
||||
uploadDocumentsService = TestBed.inject(UploadDocumentsService)
|
||||
fixture = TestBed.createComponent(AppComponent)
|
||||
component = fixture.componentInstance
|
||||
})
|
||||
|
||||
it('should initialize the tour service & toggle class on body for styling', fakeAsync(() => {
|
||||
jest.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
fixture.detectChanges()
|
||||
const tourSpy = jest.spyOn(tourService, 'initialize')
|
||||
component.ngOnInit()
|
||||
expect(tourSpy).toHaveBeenCalled()
|
||||
tourService.start()
|
||||
expect(document.body.classList).toContain('tour-active')
|
||||
tourService.end()
|
||||
tick(500)
|
||||
expect(document.body.classList).not.toContain('tour-active')
|
||||
}))
|
||||
|
||||
it('should display toast on document consumed with link if user has access', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
let toast: Toast
|
||||
toastService.getToasts().subscribe((toasts) => (toast = toasts[0]))
|
||||
const toastSpy = jest.spyOn(toastService, 'show')
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'onDocumentConsumptionFinished')
|
||||
.mockReturnValue(fileStatusSubject)
|
||||
component.ngOnInit()
|
||||
fileStatusSubject.next(new FileStatus())
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
expect(toast.action).not.toBeUndefined()
|
||||
})
|
||||
|
||||
it('should display toast on document consumed without link if user does not have access', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(false)
|
||||
let toast: Toast
|
||||
toastService.getToasts().subscribe((toasts) => (toast = toasts[0]))
|
||||
const toastSpy = jest.spyOn(toastService, 'show')
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'onDocumentConsumptionFinished')
|
||||
.mockReturnValue(fileStatusSubject)
|
||||
component.ngOnInit()
|
||||
fileStatusSubject.next(new FileStatus())
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
expect(toast.action).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should display toast on document added', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
const toastSpy = jest.spyOn(toastService, 'show')
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'onDocumentDetected')
|
||||
.mockReturnValue(fileStatusSubject)
|
||||
component.ngOnInit()
|
||||
fileStatusSubject.next(new FileStatus())
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should suppress dashboard notifications if set', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest.spyOn(settingsService, 'get').mockReturnValue(true)
|
||||
jest.spyOn(router, 'url', 'get').mockReturnValue('/dashboard')
|
||||
const toastSpy = jest.spyOn(toastService, 'show')
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'onDocumentDetected')
|
||||
.mockReturnValue(fileStatusSubject)
|
||||
component.ngOnInit()
|
||||
fileStatusSubject.next(new FileStatus())
|
||||
expect(toastSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should display toast on document failed', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
const toastSpy = jest.spyOn(toastService, 'showError')
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'onDocumentConsumptionFailed')
|
||||
.mockReturnValue(fileStatusSubject)
|
||||
component.ngOnInit()
|
||||
fileStatusSubject.next(new FileStatus())
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should disable drag-drop if on dashboard', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest.spyOn(router, 'url', 'get').mockReturnValueOnce('/dashboard')
|
||||
expect(component.dragDropEnabled).toBeFalsy()
|
||||
jest.spyOn(router, 'url', 'get').mockReturnValueOnce('/documents')
|
||||
expect(component.dragDropEnabled).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should enable drag-drop if user has permissions', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
expect(component.dragDropEnabled).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should disable drag-drop if user does not have permissions', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(false)
|
||||
expect(component.dragDropEnabled).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should support drag drop', fakeAsync(() => {
|
||||
expect(component.fileIsOver).toBeFalsy()
|
||||
component.fileOver()
|
||||
tick(1)
|
||||
fixture.detectChanges()
|
||||
expect(component.fileIsOver).toBeTruthy()
|
||||
const dropzone = fixture.debugElement.query(
|
||||
By.css('.global-dropzone-overlay')
|
||||
)
|
||||
expect(dropzone).not.toBeNull()
|
||||
component.fileLeave()
|
||||
tick(700)
|
||||
fixture.detectChanges()
|
||||
expect(dropzone.classes['hide']).toBeTruthy()
|
||||
// drop
|
||||
const toastSpy = jest.spyOn(toastService, 'show')
|
||||
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFiles')
|
||||
component.dropped([])
|
||||
tick(3000)
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
expect(uploadSpy).toHaveBeenCalled()
|
||||
}))
|
||||
})
|
@@ -139,104 +139,88 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
const nextBtnTitle = $localize`Next`
|
||||
const endBtnTitle = $localize`End`
|
||||
|
||||
this.tourService.initialize([
|
||||
this.tourService.initialize(
|
||||
[
|
||||
{
|
||||
anchorId: 'tour.dashboard',
|
||||
content: $localize`The dashboard can be used to show saved views, such as an 'Inbox'. Those settings are found under Settings > Saved Views once you have created some.`,
|
||||
route: '/dashboard',
|
||||
delayAfterNavigation: 500,
|
||||
isOptional: false,
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.upload-widget',
|
||||
content: $localize`Drag-and-drop documents here to start uploading or place them in the consume folder. You can also drag-and-drop documents anywhere on all other pages of the web app. Once you do, Paperless-ngx will start training its machine learning algorithms.`,
|
||||
route: '/dashboard',
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.documents',
|
||||
content: $localize`The documents list shows all of your documents and allows for filtering as well as bulk-editing. There are three different view styles: list, small cards and large cards. A list of documents currently opened for editing is shown in the sidebar.`,
|
||||
route: '/documents?sort=created&reverse=1&page=1',
|
||||
delayAfterNavigation: 500,
|
||||
placement: 'bottom',
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.documents-filter-editor',
|
||||
content: $localize`The filtering tools allow you to quickly find documents using various searches, dates, tags, etc.`,
|
||||
route: '/documents?sort=created&reverse=1&page=1',
|
||||
placement: 'bottom',
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.documents-views',
|
||||
content: $localize`Any combination of filters can be saved as a 'view' which can then be displayed on the dashboard and / or sidebar.`,
|
||||
route: '/documents?sort=created&reverse=1&page=1',
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.tags',
|
||||
content: $localize`Tags, correspondents, document types and storage paths can all be managed using these pages. They can also be created from the document edit view.`,
|
||||
route: '/tags',
|
||||
backdropConfig: {
|
||||
offset: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.file-tasks',
|
||||
content: $localize`File Tasks shows you documents that have been consumed, are waiting to be, or may have failed during the process.`,
|
||||
route: '/tasks',
|
||||
backdropConfig: {
|
||||
offset: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.settings',
|
||||
content: $localize`Check out the settings for various tweaks to the web app, toggle settings for saved views or setup e-mail checking.`,
|
||||
route: '/settings',
|
||||
backdropConfig: {
|
||||
offset: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.outro',
|
||||
title: $localize`Thank you! 🙏`,
|
||||
content:
|
||||
$localize`There are <em>tons</em> more features and info we didn't cover here, but this should get you started. Check out the documentation or visit the project on GitHub to learn more or to report issues.` +
|
||||
'<br/><br/>' +
|
||||
$localize`Lastly, on behalf of every contributor to this community-supported project, thank you for using Paperless-ngx!`,
|
||||
route: '/dashboard',
|
||||
isOptional: false,
|
||||
backdropConfig: {
|
||||
offset: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
anchorId: 'tour.dashboard',
|
||||
content: $localize`The dashboard can be used to show saved views, such as an 'Inbox'. Those settings are found under Settings > Saved Views once you have created some.`,
|
||||
route: '/dashboard',
|
||||
enableBackdrop: true,
|
||||
delayAfterNavigation: 500,
|
||||
backdropConfig: {
|
||||
offset: 10,
|
||||
},
|
||||
prevBtnTitle,
|
||||
nextBtnTitle,
|
||||
endBtnTitle,
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.upload-widget',
|
||||
content: $localize`Drag-and-drop documents here to start uploading or place them in the consume folder. You can also drag-and-drop documents anywhere on all other pages of the web app. Once you do, Paperless-ngx will start training its machine learning algorithms.`,
|
||||
route: '/dashboard',
|
||||
enableBackdrop: true,
|
||||
isOptional: true,
|
||||
prevBtnTitle,
|
||||
nextBtnTitle,
|
||||
endBtnTitle,
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.documents',
|
||||
content: $localize`The documents list shows all of your documents and allows for filtering as well as bulk-editing. There are three different view styles: list, small cards and large cards. A list of documents currently opened for editing is shown in the sidebar.`,
|
||||
route: '/documents?sort=created&reverse=1&page=1',
|
||||
delayAfterNavigation: 500,
|
||||
placement: 'bottom',
|
||||
enableBackdrop: true,
|
||||
disableScrollToAnchor: true,
|
||||
isOptional: true,
|
||||
prevBtnTitle,
|
||||
nextBtnTitle,
|
||||
endBtnTitle,
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.documents-filter-editor',
|
||||
content: $localize`The filtering tools allow you to quickly find documents using various searches, dates, tags, etc.`,
|
||||
route: '/documents?sort=created&reverse=1&page=1',
|
||||
placement: 'bottom',
|
||||
enableBackdrop: true,
|
||||
isOptional: true,
|
||||
prevBtnTitle,
|
||||
nextBtnTitle,
|
||||
endBtnTitle,
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.documents-views',
|
||||
content: $localize`Any combination of filters can be saved as a 'view' which can then be displayed on the dashboard and / or sidebar.`,
|
||||
route: '/documents?sort=created&reverse=1&page=1',
|
||||
enableBackdrop: true,
|
||||
isOptional: true,
|
||||
prevBtnTitle,
|
||||
nextBtnTitle,
|
||||
endBtnTitle,
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.tags',
|
||||
content: $localize`Tags, correspondents, document types and storage paths can all be managed using these pages. They can also be created from the document edit view.`,
|
||||
route: '/tags',
|
||||
enableBackdrop: true,
|
||||
isOptional: true,
|
||||
prevBtnTitle,
|
||||
nextBtnTitle,
|
||||
endBtnTitle,
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.file-tasks',
|
||||
content: $localize`File Tasks shows you documents that have been consumed, are waiting to be, or may have failed during the process.`,
|
||||
route: '/tasks',
|
||||
enableBackdrop: true,
|
||||
isOptional: true,
|
||||
prevBtnTitle,
|
||||
nextBtnTitle,
|
||||
endBtnTitle,
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.settings',
|
||||
content: $localize`Check out the settings for various tweaks to the web app, toggle settings for saved views or setup e-mail checking.`,
|
||||
route: '/settings',
|
||||
enableBackdrop: true,
|
||||
isOptional: true,
|
||||
prevBtnTitle,
|
||||
nextBtnTitle,
|
||||
endBtnTitle,
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.outro',
|
||||
title: $localize`Thank you! 🙏`,
|
||||
content:
|
||||
$localize`There are <em>tons</em> more features and info we didn't cover here, but this should get you started. Check out the documentation or visit the project on GitHub to learn more or to report issues.` +
|
||||
'<br/><br/>' +
|
||||
$localize`Lastly, on behalf of every contributor to this community-supported project, thank you for using Paperless-ngx!`,
|
||||
route: '/dashboard',
|
||||
prevBtnTitle,
|
||||
nextBtnTitle,
|
||||
endBtnTitle,
|
||||
},
|
||||
])
|
||||
useLegacyTitle: true,
|
||||
}
|
||||
)
|
||||
|
||||
this.tourService.start$.subscribe(() => {
|
||||
this.renderer.addClass(document.body, 'tour-active')
|
||||
|
@@ -92,6 +92,8 @@ import { PermissionsDialogComponent } from './components/common/permissions-dial
|
||||
import { PermissionsFormComponent } from './components/common/input/permissions/permissions-form/permissions-form.component'
|
||||
import { PermissionsFilterDropdownComponent } from './components/common/permissions-filter-dropdown/permissions-filter-dropdown.component'
|
||||
import { UsernamePipe } from './pipes/username.pipe'
|
||||
import { LogoComponent } from './components/common/logo/logo.component'
|
||||
import { IsNumberPipe } from './pipes/is-number.pipe'
|
||||
|
||||
import localeAr from '@angular/common/locales/ar'
|
||||
import localeBe from '@angular/common/locales/be'
|
||||
@@ -110,10 +112,12 @@ 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 localeSk from '@angular/common/locales/sk'
|
||||
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 localeUk from '@angular/common/locales/uk'
|
||||
import localeZh from '@angular/common/locales/zh'
|
||||
|
||||
registerLocaleData(localeAr)
|
||||
@@ -134,10 +138,12 @@ registerLocaleData(localePt, 'pt-BR')
|
||||
registerLocaleData(localePt, 'pt-PT')
|
||||
registerLocaleData(localeRo)
|
||||
registerLocaleData(localeRu)
|
||||
registerLocaleData(localeSk)
|
||||
registerLocaleData(localeSl)
|
||||
registerLocaleData(localeSr)
|
||||
registerLocaleData(localeSv)
|
||||
registerLocaleData(localeTr)
|
||||
registerLocaleData(localeUk)
|
||||
registerLocaleData(localeZh)
|
||||
|
||||
function initializeApp(settings: SettingsService) {
|
||||
@@ -217,6 +223,8 @@ function initializeApp(settings: SettingsService) {
|
||||
PermissionsFormComponent,
|
||||
PermissionsFilterDropdownComponent,
|
||||
UsernamePipe,
|
||||
LogoComponent,
|
||||
IsNumberPipe,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
@@ -137,6 +137,7 @@ main {
|
||||
|
||||
.sidebar .nav-link {
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover, &.active, &:focus {
|
||||
color: var(--bs-primary);
|
||||
|
272
src-ui/src/app/components/app-frame/app-frame.component.spec.ts
Normal file
272
src-ui/src/app/components/app-frame/app-frame.component.spec.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
import {
|
||||
HttpClientTestingModule,
|
||||
HttpTestingController,
|
||||
} from '@angular/common/http/testing'
|
||||
import { AppFrameComponent } from './app-frame.component'
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||
import { RemoteVersionService } from 'src/app/services/rest/remote-version.service'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { of } from 'rxjs'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { DocumentDetailComponent } from '../document-detail/document-detail.component'
|
||||
import { SearchService } from 'src/app/services/rest/search.service'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { FILTER_FULLTEXT_QUERY } from 'src/app/data/filter-rule-type'
|
||||
import { routes } from 'src/app/app-routing.module'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
|
||||
const document = { id: 2, title: 'Hello world' }
|
||||
|
||||
describe('AppFrameComponent', () => {
|
||||
let component: AppFrameComponent
|
||||
let fixture: ComponentFixture<AppFrameComponent>
|
||||
let httpTestingController: HttpTestingController
|
||||
let settingsService: SettingsService
|
||||
let permissionsService: PermissionsService
|
||||
let remoteVersionService: RemoteVersionService
|
||||
let toastService: ToastService
|
||||
let openDocumentsService: OpenDocumentsService
|
||||
let searchService: SearchService
|
||||
let documentListViewService: DocumentListViewService
|
||||
let router: Router
|
||||
let savedViewSpy
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AppFrameComponent, IfPermissionsDirective],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
BrowserModule,
|
||||
RouterTestingModule.withRoutes(routes),
|
||||
NgbModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
providers: [
|
||||
SettingsService,
|
||||
SavedViewService,
|
||||
PermissionsService,
|
||||
RemoteVersionService,
|
||||
IfPermissionsDirective,
|
||||
ToastService,
|
||||
OpenDocumentsService,
|
||||
SearchService,
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
firstChild: {
|
||||
component: DocumentDetailComponent,
|
||||
},
|
||||
snapshot: {
|
||||
firstChild: {
|
||||
component: DocumentDetailComponent,
|
||||
params: {
|
||||
id: document.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
PermissionsGuard,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
const savedViewService = TestBed.inject(SavedViewService)
|
||||
permissionsService = TestBed.inject(PermissionsService)
|
||||
remoteVersionService = TestBed.inject(RemoteVersionService)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
openDocumentsService = TestBed.inject(OpenDocumentsService)
|
||||
searchService = TestBed.inject(SearchService)
|
||||
documentListViewService = TestBed.inject(DocumentListViewService)
|
||||
router = TestBed.inject(Router)
|
||||
|
||||
jest
|
||||
.spyOn(settingsService, 'displayName', 'get')
|
||||
.mockReturnValue('Hello World')
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
|
||||
savedViewSpy = jest.spyOn(savedViewService, 'initialize')
|
||||
|
||||
fixture = TestBed.createComponent(AppFrameComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should initialize the saved view service', () => {
|
||||
expect(savedViewSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should check for update if enabled', () => {
|
||||
const updateCheckSpy = jest.spyOn(remoteVersionService, 'checkForUpdates')
|
||||
updateCheckSpy.mockImplementation(() => {
|
||||
return of({
|
||||
version: 'v100.0',
|
||||
update_available: true,
|
||||
})
|
||||
})
|
||||
settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, true)
|
||||
component.ngOnInit()
|
||||
expect(updateCheckSpy).toHaveBeenCalled()
|
||||
fixture.detectChanges()
|
||||
expect(fixture.nativeElement.textContent).toContain('Update available')
|
||||
})
|
||||
|
||||
it('should check not for update if disabled', () => {
|
||||
const updateCheckSpy = jest.spyOn(remoteVersionService, 'checkForUpdates')
|
||||
settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, false)
|
||||
component.ngOnInit()
|
||||
fixture.detectChanges()
|
||||
expect(updateCheckSpy).not.toHaveBeenCalled()
|
||||
expect(fixture.nativeElement.textContent).not.toContain('Update available')
|
||||
})
|
||||
|
||||
it('should check for update if was disabled and then enabled', () => {
|
||||
const updateCheckSpy = jest.spyOn(remoteVersionService, 'checkForUpdates')
|
||||
settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, false)
|
||||
component.setUpdateChecking(true)
|
||||
fixture.detectChanges()
|
||||
expect(updateCheckSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show error on toggle update checking if store settings fails', () => {
|
||||
jest.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const toastSpy = jest.spyOn(toastService, 'showError')
|
||||
settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, false)
|
||||
component.setUpdateChecking(true)
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}ui_settings/`)
|
||||
.flush('error', {
|
||||
status: 500,
|
||||
statusText: 'error',
|
||||
})
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support toggling slim sidebar and saving', fakeAsync(() => {
|
||||
const saveSettingSpy = jest.spyOn(settingsService, 'set')
|
||||
expect(component.slimSidebarEnabled).toBeFalsy()
|
||||
expect(component.slimSidebarAnimating).toBeFalsy()
|
||||
component.toggleSlimSidebar()
|
||||
expect(component.slimSidebarAnimating).toBeTruthy()
|
||||
tick(200)
|
||||
expect(component.slimSidebarAnimating).toBeFalsy()
|
||||
expect(component.slimSidebarEnabled).toBeTruthy()
|
||||
expect(saveSettingSpy).toHaveBeenCalledWith(
|
||||
SETTINGS_KEYS.SLIM_SIDEBAR,
|
||||
true
|
||||
)
|
||||
}))
|
||||
|
||||
it('should show error on toggle slim sidebar if store settings fails', () => {
|
||||
jest.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const toastSpy = jest.spyOn(toastService, 'showError')
|
||||
component.toggleSlimSidebar()
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}ui_settings/`)
|
||||
.flush('error', {
|
||||
status: 500,
|
||||
statusText: 'error',
|
||||
})
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support collapsable menu', () => {
|
||||
const button: HTMLButtonElement = (
|
||||
fixture.nativeElement as HTMLDivElement
|
||||
).querySelector('button[data-toggle=collapse]')
|
||||
button.dispatchEvent(new MouseEvent('click'))
|
||||
expect(component.isMenuCollapsed).toBeFalsy()
|
||||
component.closeMenu()
|
||||
expect(component.isMenuCollapsed).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should support close document & navigate on close current doc', () => {
|
||||
const closeSpy = jest.spyOn(openDocumentsService, 'closeDocument')
|
||||
closeSpy.mockReturnValue(of(true))
|
||||
const routerSpy = jest.spyOn(router, 'navigate')
|
||||
component.closeDocument(document)
|
||||
expect(closeSpy).toHaveBeenCalledWith(document)
|
||||
expect(routerSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support close all documents & navigate on close current doc', () => {
|
||||
const closeAllSpy = jest.spyOn(openDocumentsService, 'closeAll')
|
||||
closeAllSpy.mockReturnValue(of(true))
|
||||
const routerSpy = jest.spyOn(router, 'navigate')
|
||||
component.closeAll()
|
||||
expect(closeAllSpy).toHaveBeenCalled()
|
||||
expect(routerSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should close all documents on logout', () => {
|
||||
const closeAllSpy = jest.spyOn(openDocumentsService, 'closeAll')
|
||||
component.onLogout()
|
||||
expect(closeAllSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should warn before close if dirty documents', () => {
|
||||
jest.spyOn(openDocumentsService, 'hasDirty').mockReturnValue(true)
|
||||
expect(component.canDeactivate()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should call autocomplete endpoint on input', fakeAsync(() => {
|
||||
const autocompleteSpy = jest.spyOn(searchService, 'autocomplete')
|
||||
component.searchAutoComplete(of('hello')).subscribe()
|
||||
tick(250)
|
||||
expect(autocompleteSpy).toHaveBeenCalled()
|
||||
|
||||
component.searchAutoComplete(of('hello world 1')).subscribe()
|
||||
tick(250)
|
||||
expect(autocompleteSpy).toHaveBeenCalled()
|
||||
}))
|
||||
|
||||
it('should support reset search field', () => {
|
||||
const resetSpy = jest.spyOn(component, 'resetSearchField')
|
||||
const input = (fixture.nativeElement as HTMLDivElement).querySelector(
|
||||
'input'
|
||||
) as HTMLInputElement
|
||||
input.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' }))
|
||||
expect(resetSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support choosing a search item', () => {
|
||||
expect(component.searchField.value).toEqual('')
|
||||
component.itemSelected({ item: 'hello', preventDefault: () => true })
|
||||
expect(component.searchField.value).toEqual('hello ')
|
||||
component.itemSelected({ item: 'world', preventDefault: () => true })
|
||||
expect(component.searchField.value).toEqual('hello world ')
|
||||
})
|
||||
|
||||
it('should navigate via quickFilter on search', () => {
|
||||
const str = 'hello world '
|
||||
component.searchField.patchValue(str)
|
||||
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
||||
component.search()
|
||||
expect(qfSpy).toHaveBeenCalledWith([
|
||||
{
|
||||
rule_type: FILTER_FULLTEXT_QUERY,
|
||||
value: str.trim(),
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
@@ -53,7 +53,7 @@ export class AppFrameComponent
|
||||
public settingsService: SettingsService,
|
||||
public tasksService: TasksService,
|
||||
private readonly toastService: ToastService,
|
||||
private permissionsService: PermissionsService
|
||||
permissionsService: PermissionsService
|
||||
) {
|
||||
super()
|
||||
|
||||
@@ -75,7 +75,7 @@ export class AppFrameComponent
|
||||
}
|
||||
|
||||
versionString = `${environment.appTitle} ${environment.version}`
|
||||
appRemoteVersion
|
||||
appRemoteVersion: AppRemoteVersion
|
||||
|
||||
isMenuCollapsed: boolean = true
|
||||
|
||||
@@ -103,7 +103,7 @@ export class AppFrameComponent
|
||||
this.toastService.showError(
|
||||
$localize`An error occurred while saving settings.`
|
||||
)
|
||||
console.log(error)
|
||||
console.warn(error)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -236,7 +236,7 @@ export class AppFrameComponent
|
||||
this.toastService.showError(
|
||||
$localize`An error occurred while saving update checking settings.`
|
||||
)
|
||||
console.log(error)
|
||||
console.warn(error)
|
||||
},
|
||||
})
|
||||
if (enable) {
|
||||
|
@@ -0,0 +1,43 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { ClearableBadgeComponent } from './clearable-badge.component'
|
||||
|
||||
describe('ClearableBadgeComponent', () => {
|
||||
let component: ClearableBadgeComponent
|
||||
let fixture: ComponentFixture<ClearableBadgeComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ClearableBadgeComponent],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(ClearableBadgeComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support selected', () => {
|
||||
component.selected = true
|
||||
expect(component.active).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should support numbered', () => {
|
||||
component.number = 3
|
||||
fixture.detectChanges()
|
||||
expect(component.active).toBeTruthy()
|
||||
expect((fixture.nativeElement as HTMLDivElement).textContent).toContain('3')
|
||||
})
|
||||
|
||||
it('should support selected', () => {
|
||||
let clearedResult
|
||||
component.selected = true
|
||||
fixture.detectChanges()
|
||||
component.cleared.subscribe((clear) => {
|
||||
clearedResult = clear
|
||||
})
|
||||
fixture.nativeElement
|
||||
.querySelectorAll('button')[0]
|
||||
.dispatchEvent(new MouseEvent('click'))
|
||||
expect(clearedResult).toBeTruthy()
|
||||
})
|
||||
})
|
@@ -0,0 +1,99 @@
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
discardPeriodicTasks,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import { ConfirmDialogComponent } from './confirm-dialog.component'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||
import { Subject } from 'rxjs'
|
||||
|
||||
describe('ConfirmDialogComponent', () => {
|
||||
let component: ConfirmDialogComponent
|
||||
let modal: NgbActiveModal
|
||||
let fixture: ComponentFixture<ConfirmDialogComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ConfirmDialogComponent, SafeHtmlPipe],
|
||||
providers: [NgbActiveModal, SafeHtmlPipe],
|
||||
imports: [],
|
||||
}).compileComponents()
|
||||
|
||||
modal = TestBed.inject(NgbActiveModal)
|
||||
|
||||
fixture = TestBed.createComponent(ConfirmDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
component.title = 'Confirm delete'
|
||||
component.messageBold = 'Do you really want to delete document file.pdf?'
|
||||
component.message =
|
||||
'The files for this document will be deleted permanently. This operation cannot be undone.'
|
||||
component.btnClass = 'btn-danger'
|
||||
component.btnCaption = 'Delete document'
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support alternative', () => {
|
||||
let alternativeClickedResult
|
||||
let alternativeSubjectResult
|
||||
component.alternativeClicked.subscribe((result) => {
|
||||
alternativeClickedResult = true
|
||||
})
|
||||
component.alternative()
|
||||
// with subject
|
||||
const subject = new Subject<boolean>()
|
||||
component.alternativeSubject = subject
|
||||
subject.asObservable().subscribe((result) => {
|
||||
alternativeSubjectResult = result
|
||||
})
|
||||
component.alternative()
|
||||
expect(alternativeClickedResult).toBeTruthy()
|
||||
expect(alternativeSubjectResult).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should support confirm', () => {
|
||||
let confirmClickedResult
|
||||
let confirmSubjectResult
|
||||
component.confirmClicked.subscribe((result) => {
|
||||
confirmClickedResult = true
|
||||
})
|
||||
component.confirm()
|
||||
// with subject
|
||||
const subject = new Subject<boolean>()
|
||||
component.confirmSubject = subject
|
||||
subject.asObservable().subscribe((result) => {
|
||||
confirmSubjectResult = result
|
||||
})
|
||||
component.confirm()
|
||||
expect(confirmClickedResult).toBeTruthy()
|
||||
expect(confirmSubjectResult).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should support cancel & close modal', () => {
|
||||
let confirmSubjectResult
|
||||
const closeModalSpy = jest.spyOn(modal, 'close')
|
||||
component.cancel()
|
||||
const subject = new Subject<boolean>()
|
||||
component.confirmSubject = subject
|
||||
subject.asObservable().subscribe((result) => {
|
||||
confirmSubjectResult = result
|
||||
})
|
||||
component.cancel()
|
||||
// with subject
|
||||
expect(closeModalSpy).toHaveBeenCalled()
|
||||
expect(confirmSubjectResult).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should support delay confirm', fakeAsync(() => {
|
||||
component.confirmButtonEnabled = false
|
||||
component.delayConfirm(1)
|
||||
expect(component.confirmButtonEnabled).toBeFalsy()
|
||||
tick(1500)
|
||||
fixture.detectChanges()
|
||||
expect(component.confirmButtonEnabled).toBeTruthy()
|
||||
discardPeriodicTasks()
|
||||
}))
|
||||
})
|
@@ -0,0 +1,141 @@
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
let fixture: ComponentFixture<DateDropdownComponent>
|
||||
import {
|
||||
DateDropdownComponent,
|
||||
DateSelection,
|
||||
RelativeDate,
|
||||
} from './date-dropdown.component'
|
||||
import {
|
||||
HttpClientTestingModule,
|
||||
HttpTestingController,
|
||||
} from '@angular/common/http/testing'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { DatePipe } from '@angular/common'
|
||||
|
||||
describe('DateDropdownComponent', () => {
|
||||
let component: DateDropdownComponent
|
||||
let httpTestingController: HttpTestingController
|
||||
let settingsService: SettingsService
|
||||
let settingsSpy
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
DateDropdownComponent,
|
||||
ClearableBadgeComponent,
|
||||
CustomDatePipe,
|
||||
],
|
||||
providers: [SettingsService, CustomDatePipe, DatePipe],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
NgbModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
settingsSpy = jest.spyOn(settingsService, 'getLocalizedDateInputFormat')
|
||||
|
||||
fixture = TestBed.createComponent(DateDropdownComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should use a localized date placeholder', () => {
|
||||
expect(component.datePlaceHolder).toEqual('mm/dd/yyyy')
|
||||
expect(settingsSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support date input, emit change', fakeAsync(() => {
|
||||
let result: string
|
||||
component.dateAfterChange.subscribe((date) => (result = date))
|
||||
const input: HTMLInputElement = fixture.nativeElement.querySelector('input')
|
||||
input.value = '5/30/2023'
|
||||
input.dispatchEvent(new Event('change'))
|
||||
tick(500)
|
||||
expect(result).not.toBeNull()
|
||||
}))
|
||||
|
||||
it('should support date select, emit datesSet change', fakeAsync(() => {
|
||||
let result: DateSelection
|
||||
component.datesSet.subscribe((date) => (result = date))
|
||||
const input: HTMLInputElement = fixture.nativeElement.querySelector('input')
|
||||
input.value = '5/30/2023'
|
||||
input.dispatchEvent(new Event('dateSelect'))
|
||||
tick(500)
|
||||
expect(result).not.toBeNull()
|
||||
}))
|
||||
|
||||
it('should support relative dates', fakeAsync(() => {
|
||||
let result: DateSelection
|
||||
component.datesSet.subscribe((date) => (result = date))
|
||||
component.setRelativeDate(null)
|
||||
component.setRelativeDate(RelativeDate.LAST_7_DAYS)
|
||||
tick(500)
|
||||
expect(result).toEqual({
|
||||
after: null,
|
||||
before: null,
|
||||
relativeDateID: RelativeDate.LAST_7_DAYS,
|
||||
})
|
||||
}))
|
||||
|
||||
it('should support report if active', () => {
|
||||
component.relativeDate = RelativeDate.LAST_7_DAYS
|
||||
expect(component.isActive).toBeTruthy()
|
||||
component.relativeDate = null
|
||||
component.dateAfter = '2023-05-30'
|
||||
expect(component.isActive).toBeTruthy()
|
||||
component.dateAfter = null
|
||||
component.dateBefore = '2023-05-30'
|
||||
expect(component.isActive).toBeTruthy()
|
||||
component.dateBefore = null
|
||||
expect(component.isActive).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should support reset', () => {
|
||||
component.dateAfter = '2023-05-30'
|
||||
component.reset()
|
||||
expect(component.dateAfter).toBeNull()
|
||||
})
|
||||
|
||||
it('should support clearAfter', () => {
|
||||
component.dateAfter = '2023-05-30'
|
||||
component.clearAfter()
|
||||
expect(component.dateAfter).toBeNull()
|
||||
})
|
||||
|
||||
it('should support clearBefore', () => {
|
||||
component.dateBefore = '2023-05-30'
|
||||
component.clearBefore()
|
||||
expect(component.dateBefore).toBeNull()
|
||||
})
|
||||
|
||||
it('should limit keyboard events', () => {
|
||||
const input: HTMLInputElement = fixture.nativeElement.querySelector('input')
|
||||
let event: KeyboardEvent = new KeyboardEvent('keypress', {
|
||||
key: '9',
|
||||
})
|
||||
let eventSpy = jest.spyOn(event, 'preventDefault')
|
||||
input.dispatchEvent(event)
|
||||
expect(eventSpy).not.toHaveBeenCalled()
|
||||
|
||||
event = new KeyboardEvent('keypress', {
|
||||
key: '{',
|
||||
})
|
||||
eventSpy = jest.spyOn(event, 'preventDefault')
|
||||
input.dispatchEvent(event)
|
||||
expect(eventSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
@@ -1,4 +1,3 @@
|
||||
import { formatDate } from '@angular/common'
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
|
@@ -0,0 +1,55 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog.component'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { EditDialogMode } from '../edit-dialog.component'
|
||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { SelectComponent } from '../../input/select/select.component'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
|
||||
|
||||
describe('CorrespondentEditDialogComponent', () => {
|
||||
let component: CorrespondentEditDialogComponent
|
||||
let fixture: ComponentFixture<CorrespondentEditDialogComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
CorrespondentEditDialogComponent,
|
||||
IfPermissionsDirective,
|
||||
IfOwnerDirective,
|
||||
SelectComponent,
|
||||
TextComponent,
|
||||
PermissionsFormComponent,
|
||||
],
|
||||
providers: [NgbActiveModal],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgSelectModule,
|
||||
NgbModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(CorrespondentEditDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support create and edit modes', () => {
|
||||
component.dialogMode = EditDialogMode.CREATE
|
||||
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
|
||||
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
|
||||
fixture.detectChanges()
|
||||
expect(createTitleSpy).toHaveBeenCalled()
|
||||
expect(editTitleSpy).not.toHaveBeenCalled()
|
||||
component.dialogMode = EditDialogMode.EDIT
|
||||
fixture.detectChanges()
|
||||
expect(editTitleSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
@@ -0,0 +1,55 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog.component'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { EditDialogMode } from '../edit-dialog.component'
|
||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { SelectComponent } from '../../input/select/select.component'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
|
||||
|
||||
describe('DocumentTypeEditDialogComponent', () => {
|
||||
let component: DocumentTypeEditDialogComponent
|
||||
let fixture: ComponentFixture<DocumentTypeEditDialogComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
DocumentTypeEditDialogComponent,
|
||||
IfPermissionsDirective,
|
||||
IfOwnerDirective,
|
||||
SelectComponent,
|
||||
TextComponent,
|
||||
PermissionsFormComponent,
|
||||
],
|
||||
providers: [NgbActiveModal],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgSelectModule,
|
||||
NgbModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(DocumentTypeEditDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support create and edit modes', () => {
|
||||
component.dialogMode = EditDialogMode.CREATE
|
||||
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
|
||||
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
|
||||
fixture.detectChanges()
|
||||
expect(createTitleSpy).toHaveBeenCalled()
|
||||
expect(editTitleSpy).not.toHaveBeenCalled()
|
||||
component.dialogMode = EditDialogMode.EDIT
|
||||
fixture.detectChanges()
|
||||
expect(editTitleSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
@@ -0,0 +1,234 @@
|
||||
import {
|
||||
HttpClientTestingModule,
|
||||
HttpTestingController,
|
||||
} from '@angular/common/http/testing'
|
||||
import { Component } from '@angular/core'
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import {
|
||||
FormGroup,
|
||||
FormControl,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||
import { TagService } from 'src/app/services/rest/tag.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { EditDialogComponent, EditDialogMode } from './edit-dialog.component'
|
||||
import {
|
||||
DEFAULT_MATCHING_ALGORITHM,
|
||||
MATCH_ALL,
|
||||
MATCH_AUTO,
|
||||
MATCH_NONE,
|
||||
} from 'src/app/data/matching-model'
|
||||
import { of } from 'rxjs'
|
||||
import { environment } from 'src/environments/environment'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div>
|
||||
<h4 class="modal-title" id="modal-basic-title">{{ getTitle() }}</h4>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
class TestComponent extends EditDialogComponent<PaperlessTag> {
|
||||
constructor(
|
||||
service: TagService,
|
||||
activeModal: NgbActiveModal,
|
||||
userService: UserService,
|
||||
settingsService: SettingsService
|
||||
) {
|
||||
super(service, activeModal, userService, settingsService)
|
||||
}
|
||||
|
||||
getForm(): FormGroup<any> {
|
||||
return new FormGroup({
|
||||
name: new FormControl(''),
|
||||
color: new FormControl(''),
|
||||
is_inbox_tag: new FormControl(false),
|
||||
permissions_form: new FormControl(null),
|
||||
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const currentUser = {
|
||||
id: 99,
|
||||
username: 'user99',
|
||||
}
|
||||
|
||||
const permissions = {
|
||||
view: {
|
||||
users: [11],
|
||||
groups: [],
|
||||
},
|
||||
change: {
|
||||
users: [],
|
||||
groups: [2],
|
||||
},
|
||||
}
|
||||
|
||||
const tag = {
|
||||
id: 1,
|
||||
name: 'Tag 1',
|
||||
color: '#fff000',
|
||||
is_inbox_tag: false,
|
||||
matching_algorithm: MATCH_AUTO,
|
||||
owner: 10,
|
||||
permissions,
|
||||
}
|
||||
|
||||
describe('EditDialogComponent', () => {
|
||||
let component: TestComponent
|
||||
let fixture: ComponentFixture<TestComponent>
|
||||
let tagService: TagService
|
||||
let activeModal: NgbActiveModal
|
||||
let httpTestingController: HttpTestingController
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [TestComponent],
|
||||
providers: [
|
||||
NgbActiveModal,
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {
|
||||
listAll: () =>
|
||||
of({
|
||||
results: [
|
||||
{
|
||||
id: 13,
|
||||
username: 'user1',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: SettingsService,
|
||||
useValue: {
|
||||
currentUser,
|
||||
},
|
||||
},
|
||||
TagService,
|
||||
],
|
||||
imports: [HttpClientTestingModule, FormsModule, ReactiveFormsModule],
|
||||
}).compileComponents()
|
||||
|
||||
tagService = TestBed.inject(TagService)
|
||||
activeModal = TestBed.inject(NgbActiveModal)
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
|
||||
fixture = TestBed.createComponent(TestComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should interpolate object permissions', () => {
|
||||
component.object = tag
|
||||
component.dialogMode = EditDialogMode.EDIT
|
||||
component.ngOnInit()
|
||||
|
||||
expect(component.objectForm.get('permissions_form').value).toEqual({
|
||||
owner: tag.owner,
|
||||
set_permissions: permissions,
|
||||
})
|
||||
})
|
||||
|
||||
it('should delay close enabled', fakeAsync(() => {
|
||||
expect(component.closeEnabled).toBeFalsy()
|
||||
component.ngOnInit()
|
||||
tick(100)
|
||||
expect(component.closeEnabled).toBeTruthy()
|
||||
}))
|
||||
|
||||
it('should set default owner when in create mode', () => {
|
||||
component.dialogMode = EditDialogMode.CREATE
|
||||
component.ngOnInit()
|
||||
expect(component.objectForm.get('permissions_form').value.owner).toEqual(
|
||||
currentUser.id
|
||||
)
|
||||
// cover optional chaining
|
||||
component.objectForm.removeControl('permissions_form')
|
||||
component.ngOnInit()
|
||||
})
|
||||
|
||||
it('should detect if pattern required', () => {
|
||||
expect(component.patternRequired).toBeFalsy()
|
||||
component.objectForm.get('matching_algorithm').setValue(MATCH_AUTO)
|
||||
expect(component.patternRequired).toBeFalsy()
|
||||
component.objectForm.get('matching_algorithm').setValue(MATCH_NONE)
|
||||
expect(component.patternRequired).toBeFalsy()
|
||||
component.objectForm.get('matching_algorithm').setValue(MATCH_ALL)
|
||||
expect(component.patternRequired).toBeTruthy()
|
||||
// coverage
|
||||
component.objectForm = null
|
||||
expect(component.patternRequired).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should support create and edit modes', () => {
|
||||
component.dialogMode = EditDialogMode.CREATE
|
||||
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
|
||||
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
|
||||
fixture.detectChanges()
|
||||
expect(createTitleSpy).toHaveBeenCalled()
|
||||
expect(editTitleSpy).not.toHaveBeenCalled()
|
||||
component.dialogMode = EditDialogMode.EDIT
|
||||
fixture.detectChanges()
|
||||
expect(editTitleSpy).toHaveBeenCalled()
|
||||
// coverage
|
||||
component.dialogMode = null
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should close on cancel', () => {
|
||||
const closeSpy = jest.spyOn(activeModal, 'close')
|
||||
component.cancel()
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should update an object on save in edit mode', () => {
|
||||
const updateSpy = jest.spyOn(tagService, 'update')
|
||||
component.dialogMode = EditDialogMode.EDIT
|
||||
component.save()
|
||||
expect(updateSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should create an object on save in edit mode', () => {
|
||||
const createSpy = jest.spyOn(tagService, 'create')
|
||||
component.dialogMode = EditDialogMode.CREATE
|
||||
component.save()
|
||||
expect(createSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should close on successful save', () => {
|
||||
const closeSpy = jest.spyOn(activeModal, 'close')
|
||||
const successSpy = jest.spyOn(component.succeeded, 'emit')
|
||||
component.save()
|
||||
httpTestingController.expectOne(`${environment.apiBaseUrl}tags/`).flush({})
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
expect(successSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not close on failed save', () => {
|
||||
const closeSpy = jest.spyOn(activeModal, 'close')
|
||||
const failedSpy = jest.spyOn(component.failed, 'next')
|
||||
component.save()
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}tags/`)
|
||||
.flush('error', {
|
||||
status: 500,
|
||||
statusText: 'error',
|
||||
})
|
||||
expect(closeSpy).not.toHaveBeenCalled()
|
||||
expect(failedSpy).toHaveBeenCalled()
|
||||
expect(component.error).toEqual('error')
|
||||
})
|
||||
})
|
@@ -15,9 +15,14 @@ import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { PermissionsFormObject } from '../input/permissions/permissions-form/permissions-form.component'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
|
||||
export enum EditDialogMode {
|
||||
CREATE = 0,
|
||||
EDIT = 1,
|
||||
}
|
||||
|
||||
@Directive()
|
||||
export abstract class EditDialogComponent<
|
||||
T extends ObjectWithPermissions | ObjectWithId
|
||||
T extends ObjectWithPermissions | ObjectWithId,
|
||||
> implements OnInit
|
||||
{
|
||||
constructor(
|
||||
@@ -30,7 +35,7 @@ export abstract class EditDialogComponent<
|
||||
users: PaperlessUser[]
|
||||
|
||||
@Input()
|
||||
dialogMode: string = 'create'
|
||||
dialogMode: EditDialogMode = EditDialogMode.CREATE
|
||||
|
||||
@Input()
|
||||
object: T
|
||||
@@ -71,7 +76,7 @@ export abstract class EditDialogComponent<
|
||||
|
||||
this.userService.listAll().subscribe((r) => {
|
||||
this.users = r.results
|
||||
if (this.dialogMode === 'create') {
|
||||
if (this.dialogMode === EditDialogMode.CREATE) {
|
||||
this.objectForm.get('permissions_form')?.setValue({
|
||||
owner: this.settingsService.currentUser.id,
|
||||
})
|
||||
@@ -87,15 +92,11 @@ export abstract class EditDialogComponent<
|
||||
return $localize`Edit item`
|
||||
}
|
||||
|
||||
getSaveErrorMessage(error: string) {
|
||||
return $localize`Could not save element: ${error}`
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
switch (this.dialogMode) {
|
||||
case 'create':
|
||||
case EditDialogMode.CREATE:
|
||||
return this.getCreateTitle()
|
||||
case 'edit':
|
||||
case EditDialogMode.EDIT:
|
||||
return this.getEditTitle()
|
||||
default:
|
||||
break
|
||||
@@ -127,10 +128,10 @@ export abstract class EditDialogComponent<
|
||||
var newObject = Object.assign(Object.assign({}, this.object), formValues)
|
||||
var serverResponse: Observable<T>
|
||||
switch (this.dialogMode) {
|
||||
case 'create':
|
||||
case EditDialogMode.CREATE:
|
||||
serverResponse = this.service.create(newObject)
|
||||
break
|
||||
case 'edit':
|
||||
case EditDialogMode.EDIT:
|
||||
serverResponse = this.service.update(newObject)
|
||||
default:
|
||||
break
|
||||
|
@@ -0,0 +1,57 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { EditDialogMode } from '../edit-dialog.component'
|
||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { SelectComponent } from '../../input/select/select.component'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
|
||||
import { GroupEditDialogComponent } from './group-edit-dialog.component'
|
||||
import { PermissionsSelectComponent } from '../../permissions-select/permissions-select.component'
|
||||
|
||||
describe('GroupEditDialogComponent', () => {
|
||||
let component: GroupEditDialogComponent
|
||||
let fixture: ComponentFixture<GroupEditDialogComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
GroupEditDialogComponent,
|
||||
IfPermissionsDirective,
|
||||
IfOwnerDirective,
|
||||
SelectComponent,
|
||||
TextComponent,
|
||||
PermissionsFormComponent,
|
||||
PermissionsSelectComponent,
|
||||
],
|
||||
providers: [NgbActiveModal],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgSelectModule,
|
||||
NgbModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(GroupEditDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support create and edit modes', () => {
|
||||
component.dialogMode = EditDialogMode.CREATE
|
||||
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
|
||||
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
|
||||
fixture.detectChanges()
|
||||
expect(createTitleSpy).toHaveBeenCalled()
|
||||
expect(editTitleSpy).not.toHaveBeenCalled()
|
||||
component.dialogMode = EditDialogMode.EDIT
|
||||
fixture.detectChanges()
|
||||
expect(editTitleSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
discardPeriodicTasks,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import {
|
||||
HttpClientTestingModule,
|
||||
HttpTestingController,
|
||||
} from '@angular/common/http/testing'
|
||||
import { EditDialogMode } from '../edit-dialog.component'
|
||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { SelectComponent } from '../../input/select/select.component'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
|
||||
import { MailAccountEditDialogComponent } from './mail-account-edit-dialog.component'
|
||||
import { PasswordComponent } from '../../input/password/password.component'
|
||||
import { CheckComponent } from '../../input/check/check.component'
|
||||
import { IMAPSecurity } from 'src/app/data/paperless-mail-account'
|
||||
import { environment } from 'src/environments/environment'
|
||||
|
||||
describe('MailAccountEditDialogComponent', () => {
|
||||
let component: MailAccountEditDialogComponent
|
||||
let fixture: ComponentFixture<MailAccountEditDialogComponent>
|
||||
let httpController: HttpTestingController
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
MailAccountEditDialogComponent,
|
||||
IfPermissionsDirective,
|
||||
IfOwnerDirective,
|
||||
SelectComponent,
|
||||
TextComponent,
|
||||
CheckComponent,
|
||||
PermissionsFormComponent,
|
||||
PasswordComponent,
|
||||
],
|
||||
providers: [NgbActiveModal],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgSelectModule,
|
||||
NgbModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
httpController = TestBed.inject(HttpTestingController)
|
||||
|
||||
fixture = TestBed.createComponent(MailAccountEditDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support create and edit modes', () => {
|
||||
component.dialogMode = EditDialogMode.CREATE
|
||||
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
|
||||
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
|
||||
fixture.detectChanges()
|
||||
expect(createTitleSpy).toHaveBeenCalled()
|
||||
expect(editTitleSpy).not.toHaveBeenCalled()
|
||||
component.dialogMode = EditDialogMode.EDIT
|
||||
fixture.detectChanges()
|
||||
expect(editTitleSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support test mail account and show appropriate expiring alert', fakeAsync(() => {
|
||||
component.object = {
|
||||
name: 'example',
|
||||
imap_server: 'imap.example.com',
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
imap_port: 443,
|
||||
imap_security: IMAPSecurity.SSL,
|
||||
is_token: false,
|
||||
}
|
||||
|
||||
// success
|
||||
component.test()
|
||||
httpController
|
||||
.expectOne(`${environment.apiBaseUrl}mail_accounts/test/`)
|
||||
.flush({ success: true })
|
||||
fixture.detectChanges()
|
||||
expect(fixture.nativeElement.textContent).toContain(
|
||||
'Successfully connected'
|
||||
)
|
||||
tick(6000)
|
||||
fixture.detectChanges()
|
||||
expect(fixture.nativeElement.textContent).not.toContain(
|
||||
'Successfully connected'
|
||||
)
|
||||
|
||||
// not success
|
||||
component.test()
|
||||
httpController
|
||||
.expectOne(`${environment.apiBaseUrl}mail_accounts/test/`)
|
||||
.flush({ success: false })
|
||||
fixture.detectChanges()
|
||||
expect(fixture.nativeElement.textContent).toContain('Unable to connect')
|
||||
|
||||
// error
|
||||
component.test()
|
||||
httpController
|
||||
.expectOne(`${environment.apiBaseUrl}mail_accounts/test/`)
|
||||
.flush({}, { status: 500, statusText: 'error' })
|
||||
fixture.detectChanges()
|
||||
expect(fixture.nativeElement.textContent).toContain('Unable to connect')
|
||||
tick(6000)
|
||||
}))
|
||||
})
|
@@ -0,0 +1,113 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { EditDialogMode } from '../edit-dialog.component'
|
||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { SelectComponent } from '../../input/select/select.component'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
|
||||
import { MailRuleEditDialogComponent } from './mail-rule-edit-dialog.component'
|
||||
import { NumberComponent } from '../../input/number/number.component'
|
||||
import { TagsComponent } from '../../input/tags/tags.component'
|
||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
|
||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||
import { of } from 'rxjs'
|
||||
import {
|
||||
MailAction,
|
||||
MailMetadataCorrespondentOption,
|
||||
} from 'src/app/data/paperless-mail-rule'
|
||||
|
||||
describe('MailRuleEditDialogComponent', () => {
|
||||
let component: MailRuleEditDialogComponent
|
||||
let fixture: ComponentFixture<MailRuleEditDialogComponent>
|
||||
let accountService: MailAccountService
|
||||
let correspondentService: CorrespondentService
|
||||
let documentTypeService: DocumentTypeService
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
MailRuleEditDialogComponent,
|
||||
IfPermissionsDirective,
|
||||
IfOwnerDirective,
|
||||
SelectComponent,
|
||||
TextComponent,
|
||||
PermissionsFormComponent,
|
||||
NumberComponent,
|
||||
TagsComponent,
|
||||
SafeHtmlPipe,
|
||||
],
|
||||
providers: [
|
||||
NgbActiveModal,
|
||||
{
|
||||
provide: MailAccountService,
|
||||
useValue: {
|
||||
listAll: () => of([]),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CorrespondentService,
|
||||
useValue: {
|
||||
listAll: () => of([]),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: DocumentTypeService,
|
||||
useValue: {
|
||||
listAll: () => of([]),
|
||||
},
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgSelectModule,
|
||||
NgbModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(MailRuleEditDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support create and edit modes', () => {
|
||||
component.dialogMode = EditDialogMode.CREATE
|
||||
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
|
||||
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
|
||||
fixture.detectChanges()
|
||||
expect(createTitleSpy).toHaveBeenCalled()
|
||||
expect(editTitleSpy).not.toHaveBeenCalled()
|
||||
component.dialogMode = EditDialogMode.EDIT
|
||||
fixture.detectChanges()
|
||||
expect(editTitleSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support optional fields', () => {
|
||||
expect(component.showCorrespondentField).toBeFalsy()
|
||||
component.objectForm
|
||||
.get('assign_correspondent_from')
|
||||
.setValue(MailMetadataCorrespondentOption.FromCustom)
|
||||
expect(component.showCorrespondentField).toBeTruthy()
|
||||
|
||||
expect(component.showActionParamField).toBeFalsy()
|
||||
component.objectForm.get('action').setValue(MailAction.Move)
|
||||
expect(component.showActionParamField).toBeTruthy()
|
||||
component.objectForm.get('action').setValue('')
|
||||
expect(component.showActionParamField).toBeFalsy()
|
||||
component.objectForm.get('action').setValue(MailAction.Tag)
|
||||
expect(component.showActionParamField).toBeTruthy()
|
||||
|
||||
// coverage of optional chaining
|
||||
component.objectForm = null
|
||||
expect(component.showCorrespondentField).toBeFalsy()
|
||||
expect(component.showActionParamField).toBeFalsy()
|
||||
})
|
||||
})
|
@@ -0,0 +1,57 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { EditDialogMode } from '../edit-dialog.component'
|
||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { SelectComponent } from '../../input/select/select.component'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
|
||||
import { StoragePathEditDialogComponent } from './storage-path-edit-dialog.component'
|
||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||
|
||||
describe('StoragePathEditDialogComponent', () => {
|
||||
let component: StoragePathEditDialogComponent
|
||||
let fixture: ComponentFixture<StoragePathEditDialogComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
StoragePathEditDialogComponent,
|
||||
IfPermissionsDirective,
|
||||
IfOwnerDirective,
|
||||
SelectComponent,
|
||||
TextComponent,
|
||||
PermissionsFormComponent,
|
||||
SafeHtmlPipe,
|
||||
],
|
||||
providers: [NgbActiveModal],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgSelectModule,
|
||||
NgbModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(StoragePathEditDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support create and edit modes', () => {
|
||||
component.dialogMode = EditDialogMode.CREATE
|
||||
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
|
||||
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
|
||||
fixture.detectChanges()
|
||||
expect(createTitleSpy).toHaveBeenCalled()
|
||||
expect(editTitleSpy).not.toHaveBeenCalled()
|
||||
component.dialogMode = EditDialogMode.EDIT
|
||||
fixture.detectChanges()
|
||||
expect(editTitleSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
@@ -0,0 +1,59 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { EditDialogMode } from '../edit-dialog.component'
|
||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { SelectComponent } from '../../input/select/select.component'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
|
||||
import { TagEditDialogComponent } from './tag-edit-dialog.component'
|
||||
import { ColorComponent } from '../../input/color/color.component'
|
||||
import { CheckComponent } from '../../input/check/check.component'
|
||||
|
||||
describe('TagEditDialogComponent', () => {
|
||||
let component: TagEditDialogComponent
|
||||
let fixture: ComponentFixture<TagEditDialogComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
TagEditDialogComponent,
|
||||
IfPermissionsDirective,
|
||||
IfOwnerDirective,
|
||||
SelectComponent,
|
||||
TextComponent,
|
||||
PermissionsFormComponent,
|
||||
ColorComponent,
|
||||
CheckComponent,
|
||||
],
|
||||
providers: [NgbActiveModal],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgSelectModule,
|
||||
NgbModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(TagEditDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support create and edit modes', () => {
|
||||
component.dialogMode = EditDialogMode.CREATE
|
||||
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
|
||||
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
|
||||
fixture.detectChanges()
|
||||
expect(createTitleSpy).toHaveBeenCalled()
|
||||
expect(editTitleSpy).not.toHaveBeenCalled()
|
||||
component.dialogMode = EditDialogMode.EDIT
|
||||
fixture.detectChanges()
|
||||
expect(editTitleSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
@@ -0,0 +1,115 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { EditDialogMode } from '../edit-dialog.component'
|
||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { SelectComponent } from '../../input/select/select.component'
|
||||
import {
|
||||
AbstractControl,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
|
||||
import { UserEditDialogComponent } from './user-edit-dialog.component'
|
||||
import { PasswordComponent } from '../../input/password/password.component'
|
||||
import { PermissionsSelectComponent } from '../../permissions-select/permissions-select.component'
|
||||
import { GroupService } from 'src/app/services/rest/group.service'
|
||||
import { of } from 'rxjs'
|
||||
|
||||
describe('UserEditDialogComponent', () => {
|
||||
let component: UserEditDialogComponent
|
||||
let fixture: ComponentFixture<UserEditDialogComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
UserEditDialogComponent,
|
||||
IfPermissionsDirective,
|
||||
IfOwnerDirective,
|
||||
SelectComponent,
|
||||
TextComponent,
|
||||
PasswordComponent,
|
||||
PermissionsFormComponent,
|
||||
PermissionsSelectComponent,
|
||||
],
|
||||
providers: [
|
||||
NgbActiveModal,
|
||||
{
|
||||
provide: GroupService,
|
||||
useValue: {
|
||||
listAll: () =>
|
||||
of({
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
permissions: ['dummy_perms'],
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgSelectModule,
|
||||
NgbModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(UserEditDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support create and edit modes', () => {
|
||||
component.dialogMode = EditDialogMode.CREATE
|
||||
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
|
||||
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
|
||||
fixture.detectChanges()
|
||||
expect(createTitleSpy).toHaveBeenCalled()
|
||||
expect(editTitleSpy).not.toHaveBeenCalled()
|
||||
component.dialogMode = EditDialogMode.EDIT
|
||||
fixture.detectChanges()
|
||||
expect(editTitleSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should disable user permissions select on toggle superuser', () => {
|
||||
const control: AbstractControl =
|
||||
component.objectForm.get('user_permissions')
|
||||
expect(control.disabled).toBeFalsy()
|
||||
component.objectForm.get('is_superuser').setValue(true)
|
||||
component.onToggleSuperUser()
|
||||
expect(control.disabled).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should update inherited permissions', () => {
|
||||
component.objectForm.get('groups').setValue(null)
|
||||
expect(component.inheritedPermissions).toEqual([])
|
||||
component.objectForm.get('groups').setValue([1])
|
||||
expect(component.inheritedPermissions).toEqual(['dummy_perms'])
|
||||
component.objectForm.get('groups').setValue([2])
|
||||
expect(component.inheritedPermissions).toEqual([])
|
||||
})
|
||||
|
||||
it('should detect whether password was changed in form on save', () => {
|
||||
component.objectForm.get('password').setValue(null)
|
||||
component.save()
|
||||
expect(component.passwordIsSet).toBeFalsy()
|
||||
|
||||
// unchanged pw
|
||||
component.objectForm.get('password').setValue('*******')
|
||||
component.save()
|
||||
expect(component.passwordIsSet).toBeFalsy()
|
||||
|
||||
// unchanged pw
|
||||
component.objectForm.get('password').setValue('helloworld')
|
||||
component.save()
|
||||
expect(component.passwordIsSet).toBeTruthy()
|
||||
})
|
||||
})
|
@@ -34,7 +34,7 @@
|
||||
<div *ngIf="selectionModel.items" class="items" #buttonItems>
|
||||
<ng-container *ngFor="let item of selectionModel.itemsSorted | filter: filterText; let i = index">
|
||||
<app-toggleable-dropdown-button
|
||||
*ngIf="allowSelectNone || item.id" [item]="item" [hideCount]="hideCount(item)" [state]="selectionModel.get(item.id)" [count]="getUpdatedDocumentCount(item.id)" (toggle)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)" (click)="setButtonItemIndex(i)" [disabled]="disabled">
|
||||
*ngIf="allowSelectNone || item.id" [item]="item" [hideCount]="hideCount(item)" [state]="selectionModel.get(item.id)" [count]="getUpdatedDocumentCount(item.id)" (toggle)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)" (click)="setButtonItemIndex(i - 1)" [disabled]="disabled">
|
||||
</app-toggleable-dropdown-button>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
@@ -0,0 +1,487 @@
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import {
|
||||
ChangedItems,
|
||||
FilterableDropdownComponent,
|
||||
FilterableDropdownSelectionModel,
|
||||
Intersection,
|
||||
LogicalOperator,
|
||||
} from './filterable-dropdown.component'
|
||||
import { FilterPipe } from 'src/app/pipes/filter.pipe'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||
import {
|
||||
DEFAULT_MATCHING_ALGORITHM,
|
||||
MATCH_ALL,
|
||||
} from 'src/app/data/matching-model'
|
||||
import {
|
||||
ToggleableDropdownButtonComponent,
|
||||
ToggleableItemState,
|
||||
} from './toggleable-dropdown-button/toggleable-dropdown-button.component'
|
||||
import { TagComponent } from '../tag/tag.component'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
|
||||
|
||||
const items: PaperlessTag[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Tag1',
|
||||
is_inbox_tag: false,
|
||||
matching_algorithm: DEFAULT_MATCHING_ALGORITHM,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Tag2',
|
||||
is_inbox_tag: true,
|
||||
matching_algorithm: MATCH_ALL,
|
||||
match: 'str',
|
||||
},
|
||||
]
|
||||
|
||||
const nullItem = {
|
||||
id: null,
|
||||
name: 'Not assigned',
|
||||
}
|
||||
|
||||
let selectionModel: FilterableDropdownSelectionModel
|
||||
|
||||
describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () => {
|
||||
let component: FilterableDropdownComponent
|
||||
let fixture: ComponentFixture<FilterableDropdownComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
FilterableDropdownComponent,
|
||||
FilterPipe,
|
||||
ToggleableDropdownButtonComponent,
|
||||
TagComponent,
|
||||
ClearableBadgeComponent,
|
||||
],
|
||||
providers: [FilterPipe],
|
||||
imports: [NgbModule, FormsModule, ReactiveFormsModule],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(FilterableDropdownComponent)
|
||||
component = fixture.componentInstance
|
||||
selectionModel = new FilterableDropdownSelectionModel()
|
||||
})
|
||||
|
||||
it('should sanitize title', () => {
|
||||
expect(component.name).toBeNull()
|
||||
component.title = 'Foo Bar'
|
||||
expect(component.name).toEqual('foo_bar')
|
||||
})
|
||||
|
||||
it('should support reset', () => {
|
||||
component.items = items
|
||||
component.selectionModel = selectionModel
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
expect(selectionModel.getSelectedItems()).toHaveLength(1)
|
||||
expect(selectionModel.isDirty()).toBeTruthy()
|
||||
component.reset()
|
||||
expect(selectionModel.getSelectedItems()).toHaveLength(0)
|
||||
expect(selectionModel.isDirty()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should report document counts', () => {
|
||||
component.documentCounts = [
|
||||
{
|
||||
id: items[0].id,
|
||||
document_count: 12,
|
||||
},
|
||||
]
|
||||
expect(component.getUpdatedDocumentCount(items[0].id)).toEqual(12)
|
||||
expect(component.getUpdatedDocumentCount(items[1].id)).toBeUndefined() // coverate of optional chaining
|
||||
})
|
||||
|
||||
it('should emit change when items selected', () => {
|
||||
component.items = items
|
||||
component.selectionModel = selectionModel
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
expect(newModel).toBeUndefined()
|
||||
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
expect(selectionModel.isDirty()).toBeTruthy()
|
||||
expect(newModel.getSelectedItems()).toEqual([items[0]])
|
||||
expect(newModel.getExcludedItems()).toEqual([])
|
||||
|
||||
selectionModel.set(items[0].id, ToggleableItemState.NotSelected)
|
||||
expect(newModel.getSelectedItems()).toEqual([])
|
||||
|
||||
expect(component.items).toEqual([nullItem, ...items])
|
||||
})
|
||||
|
||||
it('should emit change when items excluded', () => {
|
||||
component.items = items
|
||||
component.selectionModel = selectionModel
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
expect(newModel).toBeUndefined()
|
||||
selectionModel.toggle(items[0].id)
|
||||
expect(newModel.getSelectedItems()).toEqual([items[0]])
|
||||
})
|
||||
|
||||
it('should emit change when items excluded', () => {
|
||||
component.items = items
|
||||
component.selectionModel = selectionModel
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Excluded)
|
||||
expect(newModel.getSelectedItems()).toEqual([])
|
||||
expect(newModel.getExcludedItems()).toEqual([items[0]])
|
||||
|
||||
selectionModel.set(items[0].id, ToggleableItemState.NotSelected)
|
||||
expect(newModel.getSelectedItems()).toEqual([])
|
||||
expect(newModel.getExcludedItems()).toEqual([])
|
||||
})
|
||||
|
||||
it('should exclude items when excluded and not editing', () => {
|
||||
component.items = items
|
||||
component.manyToOne = true
|
||||
component.selectionModel = selectionModel
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
component.excludeClicked(items[0].id)
|
||||
expect(selectionModel.getSelectedItems()).toEqual([])
|
||||
expect(selectionModel.getExcludedItems()).toEqual([items[0]])
|
||||
})
|
||||
|
||||
it('should toggle when items excluded and editing', () => {
|
||||
component.items = items
|
||||
component.manyToOne = true
|
||||
component.editing = true
|
||||
component.selectionModel = selectionModel
|
||||
selectionModel.set(items[0].id, ToggleableItemState.NotSelected)
|
||||
component.excludeClicked(items[0].id)
|
||||
expect(selectionModel.getSelectedItems()).toEqual([items[0]])
|
||||
expect(selectionModel.getExcludedItems()).toEqual([])
|
||||
})
|
||||
|
||||
it('should hide count for item if adding will increase size of set', () => {
|
||||
component.items = items
|
||||
component.manyToOne = true
|
||||
component.selectionModel = selectionModel
|
||||
expect(component.hideCount(items[0])).toBeFalsy()
|
||||
selectionModel.logicalOperator = LogicalOperator.Or
|
||||
expect(component.hideCount(items[0])).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should enforce single select when editing', () => {
|
||||
component.editing = true
|
||||
component.items = items
|
||||
component.selectionModel = selectionModel
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
|
||||
expect(selectionModel.singleSelect).toEqual(true)
|
||||
selectionModel.toggle(items[0].id)
|
||||
selectionModel.toggle(items[1].id)
|
||||
expect(newModel.getSelectedItems()).toEqual([items[1]])
|
||||
})
|
||||
|
||||
it('should support manyToOne selecting', () => {
|
||||
component.items = items
|
||||
selectionModel.manyToOne = false
|
||||
component.selectionModel = selectionModel
|
||||
component.manyToOne = true
|
||||
expect(component.manyToOne).toBeTruthy()
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
|
||||
expect(selectionModel.singleSelect).toEqual(false)
|
||||
selectionModel.toggle(items[0].id)
|
||||
selectionModel.toggle(items[1].id)
|
||||
expect(newModel.getSelectedItems()).toEqual([items[0], items[1]])
|
||||
})
|
||||
|
||||
it('should dynamically enable / disable modifier toggle', () => {
|
||||
component.items = items
|
||||
component.selectionModel = selectionModel
|
||||
expect(component.modifierToggleEnabled).toBeTruthy()
|
||||
selectionModel.toggle(null)
|
||||
expect(component.modifierToggleEnabled).toBeFalsy()
|
||||
component.manyToOne = true
|
||||
expect(component.modifierToggleEnabled).toBeFalsy()
|
||||
selectionModel.toggle(items[0].id)
|
||||
selectionModel.toggle(items[1].id)
|
||||
expect(component.modifierToggleEnabled).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should apply changes and close when apply button clicked', () => {
|
||||
component.items = items
|
||||
component.editing = true
|
||||
component.selectionModel = selectionModel
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
.dispatchEvent(new MouseEvent('click')) // open
|
||||
selectionModel.toggle(items[0].id)
|
||||
fixture.detectChanges()
|
||||
expect(component.modelIsDirty).toBeTruthy()
|
||||
let applyResult: ChangedItems
|
||||
const closeSpy = jest.spyOn(component.dropdown, 'close')
|
||||
component.apply.subscribe((result) => (applyResult = result))
|
||||
const applyButton = Array.from(
|
||||
(fixture.nativeElement as HTMLDivElement).querySelectorAll('button')
|
||||
).find((b) => b.textContent.includes('Apply'))
|
||||
applyButton.dispatchEvent(new MouseEvent('click'))
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
expect(applyResult).toEqual({ itemsToAdd: [items[0]], itemsToRemove: [] })
|
||||
})
|
||||
|
||||
it('should apply on close if enabled', () => {
|
||||
component.items = items
|
||||
component.editing = true
|
||||
component.applyOnClose = true
|
||||
component.selectionModel = selectionModel
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
.dispatchEvent(new MouseEvent('click')) // open
|
||||
selectionModel.toggle(items[0].id)
|
||||
fixture.detectChanges()
|
||||
expect(component.modelIsDirty).toBeTruthy()
|
||||
let applyResult: ChangedItems
|
||||
component.apply.subscribe((result) => (applyResult = result))
|
||||
component.dropdown.close()
|
||||
expect(applyResult).toEqual({ itemsToAdd: [items[0]], itemsToRemove: [] })
|
||||
})
|
||||
|
||||
it('should focus text filter on open, support filtering, clear on close', fakeAsync(() => {
|
||||
component.items = items
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
.dispatchEvent(new MouseEvent('click')) // open
|
||||
fixture.detectChanges()
|
||||
tick(100)
|
||||
expect(document.activeElement).toEqual(
|
||||
component.listFilterTextInput.nativeElement
|
||||
)
|
||||
expect(
|
||||
Array.from(
|
||||
(fixture.nativeElement as HTMLDivElement).querySelectorAll('button')
|
||||
).filter((b) => b.textContent.includes('Tag'))
|
||||
).toHaveLength(2)
|
||||
component.filterText = 'Tag2'
|
||||
fixture.detectChanges()
|
||||
expect(
|
||||
Array.from(
|
||||
(fixture.nativeElement as HTMLDivElement).querySelectorAll('button')
|
||||
).filter((b) => b.textContent.includes('Tag'))
|
||||
).toHaveLength(1)
|
||||
component.dropdown.close()
|
||||
expect(component.filterText).toHaveLength(0)
|
||||
}))
|
||||
|
||||
it('should toggle & close on enter inside filter field if 1 item remains', fakeAsync(() => {
|
||||
component.items = items
|
||||
expect(component.selectionModel.getSelectedItems()).toEqual([])
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
.dispatchEvent(new MouseEvent('click')) // open
|
||||
fixture.detectChanges()
|
||||
tick(100)
|
||||
component.filterText = 'Tag2'
|
||||
fixture.detectChanges()
|
||||
const closeSpy = jest.spyOn(component.dropdown, 'close')
|
||||
component.listFilterTextInput.nativeElement.dispatchEvent(
|
||||
new KeyboardEvent('keyup', { key: 'Enter' })
|
||||
)
|
||||
expect(component.selectionModel.getSelectedItems()).toEqual([items[1]])
|
||||
tick(300)
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
}))
|
||||
|
||||
it('should apply & close on enter inside filter field if 1 item remains if editing', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.editing = true
|
||||
let applyResult: ChangedItems
|
||||
component.apply.subscribe((result) => (applyResult = result))
|
||||
expect(component.selectionModel.getSelectedItems()).toEqual([])
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
.dispatchEvent(new MouseEvent('click')) // open
|
||||
fixture.detectChanges()
|
||||
tick(100)
|
||||
component.filterText = 'Tag2'
|
||||
fixture.detectChanges()
|
||||
component.listFilterTextInput.nativeElement.dispatchEvent(
|
||||
new KeyboardEvent('keyup', { key: 'Enter' })
|
||||
)
|
||||
expect(component.selectionModel.getSelectedItems()).toEqual([items[1]])
|
||||
tick(300)
|
||||
expect(applyResult).toEqual({ itemsToAdd: [items[1]], itemsToRemove: [] })
|
||||
}))
|
||||
|
||||
it('should support arrow keyboard navigation', fakeAsync(() => {
|
||||
component.items = items
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
.dispatchEvent(new MouseEvent('click')) // open
|
||||
fixture.detectChanges()
|
||||
tick(100)
|
||||
const filterInputEl: HTMLInputElement =
|
||||
component.listFilterTextInput.nativeElement
|
||||
expect(document.activeElement).toEqual(filterInputEl)
|
||||
const itemButtons = Array.from(
|
||||
(fixture.nativeElement as HTMLDivElement).querySelectorAll('button')
|
||||
).filter((b) => b.textContent.includes('Tag'))
|
||||
filterInputEl.dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
|
||||
)
|
||||
expect(document.activeElement).toEqual(itemButtons[0])
|
||||
itemButtons[0].dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
|
||||
)
|
||||
expect(document.activeElement).toEqual(itemButtons[1])
|
||||
itemButtons[1].dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })
|
||||
)
|
||||
expect(document.activeElement).toEqual(itemButtons[0])
|
||||
itemButtons[0].dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })
|
||||
)
|
||||
expect(document.activeElement).toEqual(filterInputEl)
|
||||
filterInputEl.value = 'foo'
|
||||
component.filterText = 'foo'
|
||||
|
||||
// dont move focus if we're traversing the field
|
||||
filterInputEl.selectionStart = 1
|
||||
expect(document.activeElement).toEqual(filterInputEl)
|
||||
|
||||
// now we're at end, so move focus
|
||||
filterInputEl.selectionStart = 3
|
||||
filterInputEl.dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
|
||||
)
|
||||
expect(document.activeElement).toEqual(itemButtons[0])
|
||||
}))
|
||||
|
||||
it('should support arrow keyboard navigation after tab keyboard navigation', fakeAsync(() => {
|
||||
component.items = items
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
.dispatchEvent(new MouseEvent('click')) // open
|
||||
fixture.detectChanges()
|
||||
tick(100)
|
||||
const filterInputEl: HTMLInputElement =
|
||||
component.listFilterTextInput.nativeElement
|
||||
expect(document.activeElement).toEqual(filterInputEl)
|
||||
const itemButtons = Array.from(
|
||||
(fixture.nativeElement as HTMLDivElement).querySelectorAll('button')
|
||||
).filter((b) => b.textContent.includes('Tag'))
|
||||
filterInputEl.dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'Tab', bubbles: true })
|
||||
)
|
||||
itemButtons[0].focus() // normally handled by browser
|
||||
itemButtons[0].dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'Tab', bubbles: true })
|
||||
)
|
||||
itemButtons[1].focus() // normally handled by browser
|
||||
itemButtons[1].dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
key: 'Tab',
|
||||
shiftKey: true,
|
||||
bubbles: true,
|
||||
})
|
||||
)
|
||||
itemButtons[0].focus() // normally handled by browser
|
||||
itemButtons[0].dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
|
||||
)
|
||||
expect(document.activeElement).toEqual(itemButtons[1])
|
||||
}))
|
||||
|
||||
it('should support arrow keyboard navigation after click', fakeAsync(() => {
|
||||
component.items = items
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
.dispatchEvent(new MouseEvent('click')) // open
|
||||
fixture.detectChanges()
|
||||
tick(100)
|
||||
const filterInputEl: HTMLInputElement =
|
||||
component.listFilterTextInput.nativeElement
|
||||
expect(document.activeElement).toEqual(filterInputEl)
|
||||
const itemButtons = Array.from(
|
||||
(fixture.nativeElement as HTMLDivElement).querySelectorAll('button')
|
||||
).filter((b) => b.textContent.includes('Tag'))
|
||||
fixture.nativeElement
|
||||
.querySelector('app-toggleable-dropdown-button')
|
||||
.dispatchEvent(new MouseEvent('click'))
|
||||
itemButtons[0].focus() // normally handled by browser
|
||||
expect(document.activeElement).toEqual(itemButtons[0])
|
||||
itemButtons[0].dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
|
||||
)
|
||||
expect(document.activeElement).toEqual(itemButtons[1])
|
||||
}))
|
||||
|
||||
it('should toggle logical operator', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.manyToOne = true
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
selectionModel.set(items[1].id, ToggleableItemState.Selected)
|
||||
component.selectionModel = selectionModel
|
||||
let changedResult: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe(
|
||||
(result) => (changedResult = result)
|
||||
)
|
||||
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
.dispatchEvent(new MouseEvent('click')) // open
|
||||
fixture.detectChanges()
|
||||
tick(100)
|
||||
|
||||
expect(component.modifierToggleEnabled).toBeTruthy()
|
||||
const operatorButtons: HTMLInputElement[] = Array.from(
|
||||
(fixture.nativeElement as HTMLDivElement).querySelectorAll('input')
|
||||
).filter((b) => ['and', 'or'].includes(b.value))
|
||||
expect(operatorButtons[0].checked).toBeTruthy()
|
||||
operatorButtons[1].dispatchEvent(new MouseEvent('click'))
|
||||
fixture.detectChanges()
|
||||
expect(selectionModel.logicalOperator).toEqual(LogicalOperator.Or)
|
||||
expect(changedResult.logicalOperator).toEqual(LogicalOperator.Or)
|
||||
}))
|
||||
|
||||
it('should toggle intersection include / exclude', fakeAsync(() => {
|
||||
component.items = items
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
selectionModel.set(items[1].id, ToggleableItemState.Selected)
|
||||
component.selectionModel = selectionModel
|
||||
let changedResult: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe(
|
||||
(result) => (changedResult = result)
|
||||
)
|
||||
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
.dispatchEvent(new MouseEvent('click')) // open
|
||||
fixture.detectChanges()
|
||||
tick(100)
|
||||
|
||||
expect(component.modifierToggleEnabled).toBeTruthy()
|
||||
const intersectionButtons: HTMLInputElement[] = Array.from(
|
||||
(fixture.nativeElement as HTMLDivElement).querySelectorAll('input')
|
||||
).filter((b) => ['include', 'exclude'].includes(b.value))
|
||||
expect(intersectionButtons[0].checked).toBeTruthy()
|
||||
intersectionButtons[1].dispatchEvent(new MouseEvent('click'))
|
||||
fixture.detectChanges()
|
||||
expect(selectionModel.intersection).toEqual(Intersection.Exclude)
|
||||
expect(changedResult.intersection).toEqual(Intersection.Exclude)
|
||||
expect(changedResult.getSelectedItems()).toEqual([])
|
||||
expect(changedResult.getExcludedItems()).toEqual(items)
|
||||
}))
|
||||
|
||||
it('FilterableDropdownSelectionModel should sort items by state', () => {
|
||||
component.items = items
|
||||
component.selectionModel = selectionModel
|
||||
selectionModel.toggle(items[1].id)
|
||||
selectionModel.apply()
|
||||
expect(selectionModel.itemsSorted).toEqual([nullItem, items[1], items[0]])
|
||||
})
|
||||
})
|
@@ -96,7 +96,7 @@ export class FilterableDropdownSelectionModel {
|
||||
toggle(id: number, fireEvent = true) {
|
||||
let state = this.temporarySelectionStates.get(id)
|
||||
if (
|
||||
state == null ||
|
||||
state == undefined ||
|
||||
(state != ToggleableItemState.Selected &&
|
||||
state != ToggleableItemState.Excluded)
|
||||
) {
|
||||
|
@@ -0,0 +1,79 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import {
|
||||
ToggleableDropdownButtonComponent,
|
||||
ToggleableItemState,
|
||||
} from './toggleable-dropdown-button.component'
|
||||
import { TagComponent } from '../../tag/tag.component'
|
||||
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||
|
||||
describe('ToggleableDropdownButtonComponent', () => {
|
||||
let component: ToggleableDropdownButtonComponent
|
||||
let fixture: ComponentFixture<ToggleableDropdownButtonComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ToggleableDropdownButtonComponent, TagComponent],
|
||||
providers: [],
|
||||
imports: [],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(ToggleableDropdownButtonComponent)
|
||||
component = fixture.componentInstance
|
||||
})
|
||||
|
||||
it('should recognize a tag', () => {
|
||||
component.item = {
|
||||
id: 1,
|
||||
name: 'Test Tag',
|
||||
is_inbox_tag: false,
|
||||
} as PaperlessTag
|
||||
|
||||
fixture.detectChanges()
|
||||
expect(component.isTag).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should report toggled state', () => {
|
||||
expect(component.isChecked()).toBeFalsy()
|
||||
expect(component.isPartiallyChecked()).toBeFalsy()
|
||||
expect(component.isExcluded()).toBeFalsy()
|
||||
|
||||
component.state = ToggleableItemState.Selected
|
||||
expect(component.isChecked()).toBeTruthy()
|
||||
expect(component.isPartiallyChecked()).toBeFalsy()
|
||||
expect(component.isExcluded()).toBeFalsy()
|
||||
|
||||
component.state = ToggleableItemState.PartiallySelected
|
||||
expect(component.isPartiallyChecked()).toBeTruthy()
|
||||
expect(component.isChecked()).toBeFalsy()
|
||||
expect(component.isExcluded()).toBeFalsy()
|
||||
|
||||
component.state = ToggleableItemState.Excluded
|
||||
expect(component.isExcluded()).toBeTruthy()
|
||||
expect(component.isChecked()).toBeFalsy()
|
||||
expect(component.isPartiallyChecked()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should emit exclude event when selected and then toggled', () => {
|
||||
let excludeResult
|
||||
let toggleResult
|
||||
component.state = ToggleableItemState.Selected
|
||||
component.exclude.subscribe(() => (excludeResult = true))
|
||||
component.toggle.subscribe(() => (toggleResult = true))
|
||||
const button = fixture.nativeElement.querySelector('button')
|
||||
button.dispatchEvent(new MouseEvent('click'))
|
||||
expect(excludeResult).toBeTruthy()
|
||||
expect(toggleResult).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should emit toggle event when not selected and then toggled', () => {
|
||||
let excludeResult
|
||||
let toggleResult
|
||||
component.state = ToggleableItemState.Excluded
|
||||
component.exclude.subscribe(() => (excludeResult = true))
|
||||
component.toggle.subscribe(() => (toggleResult = true))
|
||||
const button = fixture.nativeElement.querySelector('button')
|
||||
button.dispatchEvent(new MouseEvent('click'))
|
||||
expect(excludeResult).toBeFalsy()
|
||||
expect(toggleResult).toBeTruthy()
|
||||
})
|
||||
})
|
@@ -0,0 +1,55 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { AbstractInputComponent } from './abstract-input'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div>
|
||||
<input
|
||||
#inputField
|
||||
type="text"
|
||||
class="form-control"
|
||||
[class.is-invalid]="error"
|
||||
[id]="inputId"
|
||||
[(ngModel)]="value"
|
||||
(change)="onChange(value)"
|
||||
[disabled]="disabled"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
class TestComponent extends AbstractInputComponent<string> {
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
}
|
||||
|
||||
describe(`AbstractInputComponent`, () => {
|
||||
let component: TestComponent
|
||||
let fixture: ComponentFixture<TestComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [TestComponent],
|
||||
providers: [],
|
||||
imports: [FormsModule, ReactiveFormsModule],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(TestComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should assign uuid', () => {
|
||||
component.ngOnInit()
|
||||
expect(component.inputId).not.toBeUndefined()
|
||||
})
|
||||
|
||||
it('should support focus', () => {
|
||||
const focusSpy = jest.spyOn(component.inputField.nativeElement, 'focus')
|
||||
component.focus()
|
||||
expect(focusSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
@@ -1,5 +1,5 @@
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()" [disabled]="disabled">
|
||||
<input #inputField type="checkbox" class="form-check-input" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()" [disabled]="disabled">
|
||||
<label class="form-check-label" [for]="inputId">{{title}}</label>
|
||||
<div *ngIf="hint" class="form-text text-muted">{{hint}}</div>
|
||||
</div>
|
||||
|
@@ -0,0 +1,39 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { CheckComponent } from './check.component'
|
||||
import {
|
||||
FormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
|
||||
describe('CheckComponent', () => {
|
||||
let component: CheckComponent
|
||||
let fixture: ComponentFixture<CheckComponent>
|
||||
let input: HTMLInputElement
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [CheckComponent],
|
||||
providers: [],
|
||||
imports: [FormsModule, ReactiveFormsModule],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(CheckComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
input = component.inputField.nativeElement
|
||||
})
|
||||
|
||||
it('should support use of checkbox', () => {
|
||||
input.checked = true
|
||||
input.dispatchEvent(new Event('change'))
|
||||
fixture.detectChanges()
|
||||
expect(component.value).toBeTruthy()
|
||||
|
||||
input.checked = false
|
||||
input.dispatchEvent(new Event('change'))
|
||||
fixture.detectChanges()
|
||||
expect(component.value).toBeFalsy()
|
||||
})
|
||||
})
|
@@ -1,6 +1,5 @@
|
||||
import { Component, forwardRef, Input, OnInit } from '@angular/core'
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { Component, forwardRef } from '@angular/core'
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { AbstractInputComponent } from '../abstract-input'
|
||||
|
||||
@Component({
|
||||
|
@@ -11,7 +11,7 @@
|
||||
|
||||
</ng-template>
|
||||
|
||||
<input class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [autoClose]="'outside'" [ngbPopover]="popContent" placement="bottom" popoverClass="shadow">
|
||||
<input #inputField class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [autoClose]="'outside'" [ngbPopover]="popContent" placement="bottom" popoverClass="shadow">
|
||||
|
||||
<button class="btn btn-outline-secondary" type="button" (click)="randomize()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-dice-5" viewBox="0 0 16 16">
|
||||
|
@@ -0,0 +1,72 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import {
|
||||
FormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { ColorComponent } from './color.component'
|
||||
import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ColorSliderModule } from 'ngx-color/slider'
|
||||
|
||||
describe('ColorComponent', () => {
|
||||
let component: ColorComponent
|
||||
let fixture: ComponentFixture<ColorComponent>
|
||||
let input: HTMLInputElement
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ColorComponent],
|
||||
providers: [],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbPopoverModule,
|
||||
ColorSliderModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(ColorComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
input = component.inputField.nativeElement
|
||||
})
|
||||
|
||||
it('should support use of input', () => {
|
||||
input.value = '#ff0000'
|
||||
component.colorChanged(input.value)
|
||||
fixture.detectChanges()
|
||||
expect(component.value).toEqual('#ff0000')
|
||||
})
|
||||
|
||||
it('should set swatch color', () => {
|
||||
const swatch: HTMLSpanElement = fixture.nativeElement.querySelector(
|
||||
'span.input-group-text'
|
||||
)
|
||||
expect(swatch.style.backgroundColor).toEqual('')
|
||||
component.value = '#ff0000'
|
||||
fixture.detectChanges()
|
||||
expect(swatch.style.backgroundColor).toEqual('rgb(255, 0, 0)')
|
||||
})
|
||||
|
||||
it('should show color slider popover', () => {
|
||||
component.value = '#ff0000'
|
||||
input.dispatchEvent(new MouseEvent('click'))
|
||||
fixture.detectChanges()
|
||||
expect(
|
||||
fixture.nativeElement.querySelector('ngb-popover-window')
|
||||
).not.toBeUndefined()
|
||||
expect(
|
||||
fixture.nativeElement.querySelector('color-slider')
|
||||
).not.toBeUndefined()
|
||||
fixture.nativeElement
|
||||
.querySelector('color-slider')
|
||||
.dispatchEvent(new Event('change'))
|
||||
})
|
||||
|
||||
it('should allow randomize color and update value', () => {
|
||||
expect(component.value).toBeUndefined()
|
||||
component.randomize()
|
||||
expect(component.value).not.toBeUndefined()
|
||||
})
|
||||
})
|
@@ -1,7 +1,7 @@
|
||||
<div class="mb-3">
|
||||
<label class="form-label" [for]="inputId">{{title}}</label>
|
||||
<div class="input-group" [class.is-invalid]="error">
|
||||
<input class="form-control" [class.is-invalid]="error" [placeholder]="placeholder" [id]="inputId" maxlength="10"
|
||||
<input #inputField class="form-control" [class.is-invalid]="error" [placeholder]="placeholder" [id]="inputId" maxlength="10"
|
||||
(dateSelect)="onChange(value)" (change)="onChange(value)" (keypress)="onKeyPress($event)" (paste)="onPaste($event)"
|
||||
name="dp" [(ngModel)]="value" ngbDatepicker #datePicker="ngbDatepicker" #datePickerContent="ngModel" [disabled]="disabled">
|
||||
<button class="btn btn-outline-secondary calendar" (click)="datePicker.toggle()" type="button" [disabled]="disabled">
|
||||
@@ -9,7 +9,7 @@
|
||||
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button *ngIf="showFilter" class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="this.value === null" i18n-title title="Filter documents with this {{title}}">
|
||||
<button *ngIf="showFilter" class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="this.value === null" title="{{ fitlerButtonTitle }}">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#filter" />
|
||||
</svg>
|
||||
|
@@ -0,0 +1,103 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import {
|
||||
FormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { DateComponent } from './date.component'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import {
|
||||
NgbDateParserFormatter,
|
||||
NgbDatepickerModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import { LocalizedDateParserFormatter } from 'src/app/utils/ngb-date-parser-formatter'
|
||||
|
||||
describe('DateComponent', () => {
|
||||
let component: DateComponent
|
||||
let fixture: ComponentFixture<DateComponent>
|
||||
let input: HTMLInputElement
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [DateComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: NgbDateParserFormatter,
|
||||
useClass: LocalizedDateParserFormatter,
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
HttpClientTestingModule,
|
||||
NgbDatepickerModule,
|
||||
RouterTestingModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(DateComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
input = component.inputField.nativeElement
|
||||
})
|
||||
|
||||
it('should support use of input field', () => {
|
||||
input.value = '5/14/20'
|
||||
input.dispatchEvent(new Event('change'))
|
||||
fixture.detectChanges()
|
||||
expect(component.value).toEqual({ day: 14, month: 5, year: 2020 })
|
||||
})
|
||||
|
||||
it('should use localzed placeholder from settings', () => {
|
||||
component.ngOnInit()
|
||||
expect(component.placeholder).toEqual('mm/dd/yyyy')
|
||||
})
|
||||
|
||||
it('should support suggestions', () => {
|
||||
expect(component.value).toBeUndefined()
|
||||
component.suggestions = ['2023-05-31', '2014-05-14']
|
||||
fixture.detectChanges()
|
||||
const suggestionAnchor: HTMLAnchorElement =
|
||||
fixture.nativeElement.querySelector('a')
|
||||
suggestionAnchor.click()
|
||||
expect(component.value).toEqual({ day: 31, month: 5, year: 2023 })
|
||||
})
|
||||
|
||||
it('should limit keyboard events', () => {
|
||||
let event: KeyboardEvent = new KeyboardEvent('keypress', {
|
||||
key: '9',
|
||||
})
|
||||
let eventSpy = jest.spyOn(event, 'preventDefault')
|
||||
input.dispatchEvent(event)
|
||||
expect(eventSpy).not.toHaveBeenCalled()
|
||||
|
||||
event = new KeyboardEvent('keypress', {
|
||||
key: '{',
|
||||
})
|
||||
eventSpy = jest.spyOn(event, 'preventDefault')
|
||||
input.dispatchEvent(event)
|
||||
expect(eventSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support paste', () => {
|
||||
expect(component.value).toBeUndefined()
|
||||
const date = '5/4/20'
|
||||
const clipboardData = {
|
||||
dropEffect: null,
|
||||
effectAllowed: null,
|
||||
files: null,
|
||||
items: null,
|
||||
types: null,
|
||||
clearData: null,
|
||||
getData: () => date,
|
||||
setData: null,
|
||||
setDragImage: null,
|
||||
}
|
||||
const event = new Event('paste')
|
||||
event['clipboardData'] = clipboardData
|
||||
input.dispatchEvent(event)
|
||||
expect(component.value).toEqual({ day: 4, month: 5, year: 2020 })
|
||||
})
|
||||
})
|
@@ -98,4 +98,8 @@ export class DateComponent
|
||||
onFilterDocuments() {
|
||||
this.filterDocuments.emit([this.ngbDateParserFormatter.parse(this.value)])
|
||||
}
|
||||
|
||||
get filterButtonTitle() {
|
||||
return $localize`Filter documents with this ${this.title}`
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<div class="mb-3">
|
||||
<label class="form-label" [for]="inputId">{{title}}</label>
|
||||
<div class="input-group" [class.is-invalid]="error">
|
||||
<input type="number" class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [class.is-invalid]="error" [disabled]="disabled">
|
||||
<input #inputField type="number" class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [class.is-invalid]="error" [disabled]="disabled">
|
||||
<button *ngIf="showAdd" class="btn btn-outline-secondary" type="button" id="button-addon1" (click)="nextAsn()" [disabled]="disabled">+1</button>
|
||||
</div>
|
||||
<div class="invalid-feedback">
|
||||
|
@@ -0,0 +1,79 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import {
|
||||
FormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { NumberComponent } from './number.component'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { of } from 'rxjs'
|
||||
|
||||
describe('NumberComponent', () => {
|
||||
let component: NumberComponent
|
||||
let fixture: ComponentFixture<NumberComponent>
|
||||
let input: HTMLInputElement
|
||||
let documentService: DocumentService
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [NumberComponent],
|
||||
providers: [DocumentService],
|
||||
imports: [FormsModule, ReactiveFormsModule, HttpClientTestingModule],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(NumberComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
documentService = TestBed.inject(DocumentService)
|
||||
fixture.detectChanges()
|
||||
input = component.inputField.nativeElement
|
||||
})
|
||||
|
||||
// TODO: why doesnt this work?
|
||||
// it('should support use of input field', () => {
|
||||
// expect(component.value).toBeUndefined()
|
||||
// input.stepUp()
|
||||
// console.log(input.value);
|
||||
|
||||
// input.dispatchEvent(new Event('change'))
|
||||
// fixture.detectChanges()
|
||||
// expect(component.value).toEqual('3')
|
||||
// })
|
||||
|
||||
it('should support +1 ASN', () => {
|
||||
const listAllSpy = jest.spyOn(documentService, 'listFiltered')
|
||||
listAllSpy
|
||||
.mockReturnValueOnce(
|
||||
of({
|
||||
count: 1,
|
||||
all: [1],
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
archive_serial_number: 1000,
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
of({
|
||||
count: 0,
|
||||
all: [],
|
||||
results: [],
|
||||
})
|
||||
)
|
||||
expect(component.value).toBeUndefined()
|
||||
component.nextAsn()
|
||||
expect(component.value).toEqual(1001)
|
||||
|
||||
// this time results are empty
|
||||
component.value = undefined
|
||||
component.nextAsn()
|
||||
expect(component.value).toEqual(1)
|
||||
|
||||
component.value = 1002
|
||||
component.nextAsn()
|
||||
expect(component.value).toEqual(1002)
|
||||
})
|
||||
})
|
@@ -0,0 +1,36 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import {
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
} from '@angular/forms'
|
||||
import { PasswordComponent } from './password.component'
|
||||
|
||||
describe('PasswordComponent', () => {
|
||||
let component: PasswordComponent
|
||||
let fixture: ComponentFixture<PasswordComponent>
|
||||
let input: HTMLInputElement
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [PasswordComponent],
|
||||
providers: [],
|
||||
imports: [FormsModule, ReactiveFormsModule],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(PasswordComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
input = component.inputField.nativeElement
|
||||
})
|
||||
|
||||
it('should support use of input field', () => {
|
||||
expect(component.value).toBeUndefined()
|
||||
// TODO: why doesnt this work?
|
||||
// input.value = 'foo'
|
||||
// input.dispatchEvent(new Event('change'))
|
||||
// fixture.detectChanges()
|
||||
// expect(component.value).toEqual('foo')
|
||||
})
|
||||
})
|
@@ -0,0 +1,66 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import {
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
} from '@angular/forms'
|
||||
import { PermissionsFormComponent } from './permissions-form.component'
|
||||
import { SelectComponent } from '../../select/select.component'
|
||||
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { PermissionsGroupComponent } from '../permissions-group/permissions-group.component'
|
||||
import { PermissionsUserComponent } from '../permissions-user/permissions-user.component'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
|
||||
describe('PermissionsFormComponent', () => {
|
||||
let component: PermissionsFormComponent
|
||||
let fixture: ComponentFixture<PermissionsFormComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
PermissionsFormComponent,
|
||||
SelectComponent,
|
||||
PermissionsGroupComponent,
|
||||
PermissionsUserComponent,
|
||||
],
|
||||
providers: [],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbAccordionModule,
|
||||
HttpClientTestingModule,
|
||||
NgSelectModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(PermissionsFormComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support use of select for owner', () => {
|
||||
const changeSpy = jest.spyOn(component, 'onChange')
|
||||
component.ngOnInit()
|
||||
component.users = [
|
||||
{
|
||||
id: 2,
|
||||
username: 'foo',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
username: 'bar',
|
||||
},
|
||||
]
|
||||
component.form.get('owner').patchValue(2)
|
||||
fixture.detectChanges()
|
||||
expect(changeSpy).toHaveBeenCalledWith({
|
||||
owner: 2,
|
||||
set_permissions: {
|
||||
view: { users: [], groups: [] },
|
||||
change: { users: [], groups: [] },
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,59 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import {
|
||||
FormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { PermissionsGroupComponent } from './permissions-group.component'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { GroupService } from 'src/app/services/rest/group.service'
|
||||
import { of } from 'rxjs'
|
||||
|
||||
describe('PermissionsGroupComponent', () => {
|
||||
let component: PermissionsGroupComponent
|
||||
let fixture: ComponentFixture<PermissionsGroupComponent>
|
||||
let groupService: GroupService
|
||||
let groupServiceSpy
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [PermissionsGroupComponent],
|
||||
providers: [GroupService],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
HttpClientTestingModule,
|
||||
NgSelectModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
groupService = TestBed.inject(GroupService)
|
||||
groupServiceSpy = jest.spyOn(groupService, 'listAll').mockReturnValue(
|
||||
of({
|
||||
count: 2,
|
||||
all: [2, 3],
|
||||
results: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Group 2',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Group 3',
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
fixture = TestBed.createComponent(PermissionsGroupComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should get groups, support use of select', () => {
|
||||
component.writeValue({ id: 2, name: 'Group 2' })
|
||||
expect(component.value).toEqual({ id: 2, name: 'Group 2' })
|
||||
expect(groupServiceSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
@@ -0,0 +1,60 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import {
|
||||
FormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { PermissionsUserComponent } from './permissions-user.component'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { GroupService } from 'src/app/services/rest/group.service'
|
||||
import { of } from 'rxjs'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
|
||||
describe('PermissionsUserComponent', () => {
|
||||
let component: PermissionsUserComponent
|
||||
let fixture: ComponentFixture<PermissionsUserComponent>
|
||||
let userService: UserService
|
||||
let userServiceSpy
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [PermissionsUserComponent],
|
||||
providers: [UserService],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
HttpClientTestingModule,
|
||||
NgSelectModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
userService = TestBed.inject(UserService)
|
||||
userServiceSpy = jest.spyOn(userService, 'listAll').mockReturnValue(
|
||||
of({
|
||||
count: 2,
|
||||
all: [2, 3],
|
||||
results: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'User 2',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'User 3',
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
fixture = TestBed.createComponent(PermissionsUserComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should get users, support use of select', () => {
|
||||
component.writeValue({ id: 2, name: 'User 2' })
|
||||
expect(component.value).toEqual({ id: 2, name: 'User 2' })
|
||||
expect(userServiceSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
@@ -26,7 +26,7 @@
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus" />
|
||||
</svg>
|
||||
</button>
|
||||
<button *ngIf="showFilter" class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="isPrivate || this.value === null" i18n-title title="Filter documents with this {{title}}">
|
||||
<button *ngIf="showFilter" class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="isPrivate || this.value === null" title="{{ filterButtonTitle }}">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#filter" />
|
||||
</svg>
|
||||
|
@@ -0,0 +1,121 @@
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import {
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
} from '@angular/forms'
|
||||
import { SelectComponent } from './select.component'
|
||||
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||
import {
|
||||
DEFAULT_MATCHING_ALGORITHM,
|
||||
MATCH_ALL,
|
||||
} from 'src/app/data/matching-model'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
|
||||
const items: PaperlessTag[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Tag1',
|
||||
is_inbox_tag: false,
|
||||
matching_algorithm: DEFAULT_MATCHING_ALGORITHM,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Tag2',
|
||||
is_inbox_tag: true,
|
||||
matching_algorithm: MATCH_ALL,
|
||||
match: 'str',
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: 'Tag10',
|
||||
is_inbox_tag: false,
|
||||
matching_algorithm: DEFAULT_MATCHING_ALGORITHM,
|
||||
},
|
||||
]
|
||||
|
||||
describe('SelectComponent', () => {
|
||||
let component: SelectComponent
|
||||
let fixture: ComponentFixture<SelectComponent>
|
||||
let input: HTMLInputElement
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [SelectComponent],
|
||||
providers: [],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgSelectModule,
|
||||
RouterTestingModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(SelectComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support private items', () => {
|
||||
component.value = 3
|
||||
component.items = items
|
||||
expect(component.items).toContainEqual({
|
||||
id: 3,
|
||||
name: 'Private',
|
||||
private: true,
|
||||
})
|
||||
|
||||
component.checkForPrivateItems([4, 5])
|
||||
expect(component.items).toContainEqual({
|
||||
id: 4,
|
||||
name: 'Private',
|
||||
private: true,
|
||||
})
|
||||
expect(component.items).toContainEqual({
|
||||
id: 5,
|
||||
name: 'Private',
|
||||
private: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should support suggestions', () => {
|
||||
expect(component.value).toBeUndefined()
|
||||
component.items = items
|
||||
component.suggestions = [1, 2]
|
||||
fixture.detectChanges()
|
||||
const suggestionAnchor: HTMLAnchorElement =
|
||||
fixture.nativeElement.querySelector('a')
|
||||
suggestionAnchor.click()
|
||||
expect(component.value).toEqual(1)
|
||||
})
|
||||
|
||||
it('should support create new and emit the value', () => {
|
||||
expect(component.allowCreateNew).toBeFalsy()
|
||||
component.items = items
|
||||
let createNewVal
|
||||
component.createNew.subscribe((v) => (createNewVal = v))
|
||||
expect(component.allowCreateNew).toBeTruthy()
|
||||
component.onSearch({ term: 'foo' })
|
||||
component.addItem(undefined)
|
||||
expect(createNewVal).toEqual('foo')
|
||||
component.addItem('bar')
|
||||
expect(createNewVal).toEqual('bar')
|
||||
component.onSearch({ term: 'baz' })
|
||||
component.clickNew()
|
||||
expect(createNewVal).toEqual('baz')
|
||||
})
|
||||
|
||||
it('should clear search term on blur after delay', fakeAsync(() => {
|
||||
const clearSpy = jest.spyOn(component, 'clearLastSearchTerm')
|
||||
component.onBlur()
|
||||
tick(3000)
|
||||
expect(clearSpy).toHaveBeenCalled()
|
||||
}))
|
||||
})
|
@@ -144,4 +144,8 @@ export class SelectComponent extends AbstractInputComponent<number> {
|
||||
onFilterDocuments() {
|
||||
this.filterDocuments.emit([this.items.find((i) => i.id === this.value)])
|
||||
}
|
||||
|
||||
get filterButtonTitle() {
|
||||
return $localize`Filter documents with this ${this.title}`
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<label class="form-label" for="tags" i18n>Tags</label>
|
||||
|
||||
<div class="input-group flex-nowrap">
|
||||
<ng-select name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="value"
|
||||
<ng-select #tagSelect name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="value"
|
||||
[disabled]="disabled"
|
||||
[multiple]="true"
|
||||
[closeOnSelect]="false"
|
||||
@@ -11,11 +11,7 @@
|
||||
[addTag]="allowCreate ? createTagRef : false"
|
||||
addTagText="Add tag"
|
||||
i18n-addTagText
|
||||
(change)="onChange(value)"
|
||||
(search)="onSearch($event)"
|
||||
(focus)="clearLastSearchTerm()"
|
||||
(clear)="clearLastSearchTerm()"
|
||||
(blur)="onBlur()">
|
||||
(change)="onChange(value)">
|
||||
|
||||
<ng-template ng-label-tmp let-item="item">
|
||||
<span class="tag-wrap tag-wrap-delete" (mousedown)="removeTag($event, item.id)">
|
||||
|
@@ -0,0 +1,175 @@
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import {
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
} from '@angular/forms'
|
||||
import { TagsComponent } from './tags.component'
|
||||
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||
import {
|
||||
DEFAULT_MATCHING_ALGORITHM,
|
||||
MATCH_ALL,
|
||||
} from 'src/app/data/matching-model'
|
||||
import { NgSelectComponent, NgSelectModule } from '@ng-select/ng-select'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { of } from 'rxjs'
|
||||
import { TagService } from 'src/app/services/rest/tag.service'
|
||||
import {
|
||||
NgbAccordionModule,
|
||||
NgbModal,
|
||||
NgbModalModule,
|
||||
NgbModalRef,
|
||||
NgbPopoverModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { TagEditDialogComponent } from '../../edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
||||
import { CheckComponent } from '../check/check.component'
|
||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||
import { TextComponent } from '../text/text.component'
|
||||
import { ColorComponent } from '../color/color.component'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { PermissionsFormComponent } from '../permissions/permissions-form/permissions-form.component'
|
||||
import { SelectComponent } from '../select/select.component'
|
||||
import { ColorSliderModule } from 'ngx-color/slider'
|
||||
import { By } from '@angular/platform-browser'
|
||||
|
||||
const tags: PaperlessTag[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Tag1',
|
||||
is_inbox_tag: false,
|
||||
matching_algorithm: DEFAULT_MATCHING_ALGORITHM,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Tag2',
|
||||
is_inbox_tag: true,
|
||||
matching_algorithm: MATCH_ALL,
|
||||
match: 'str',
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: 'Tag10',
|
||||
is_inbox_tag: false,
|
||||
matching_algorithm: DEFAULT_MATCHING_ALGORITHM,
|
||||
},
|
||||
]
|
||||
|
||||
describe('TagsComponent', () => {
|
||||
let component: TagsComponent
|
||||
let fixture: ComponentFixture<TagsComponent>
|
||||
let input: HTMLInputElement
|
||||
let modalService: NgbModal
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
TagsComponent,
|
||||
TagEditDialogComponent,
|
||||
TextComponent,
|
||||
ColorComponent,
|
||||
IfOwnerDirective,
|
||||
SelectComponent,
|
||||
TextComponent,
|
||||
PermissionsFormComponent,
|
||||
ColorComponent,
|
||||
CheckComponent,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: TagService,
|
||||
useValue: {
|
||||
listAll: () =>
|
||||
of({
|
||||
results: tags,
|
||||
}),
|
||||
create: () =>
|
||||
of({
|
||||
name: 'bar',
|
||||
id: 99,
|
||||
color: '#fff000',
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgSelectModule,
|
||||
RouterTestingModule,
|
||||
HttpClientTestingModule,
|
||||
NgbModalModule,
|
||||
NgbAccordionModule,
|
||||
NgbPopoverModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
modalService = TestBed.inject(NgbModal)
|
||||
fixture = TestBed.createComponent(TagsComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
|
||||
window.PointerEvent = MouseEvent as any
|
||||
})
|
||||
|
||||
it('should support suggestions', () => {
|
||||
expect(component.value).toHaveLength(0)
|
||||
component.value = []
|
||||
component.tags = tags
|
||||
component.suggestions = [1, 2]
|
||||
fixture.detectChanges()
|
||||
const suggestionAnchor: HTMLAnchorElement =
|
||||
fixture.nativeElement.querySelector('a')
|
||||
suggestionAnchor.click()
|
||||
expect(component.value).toEqual([1])
|
||||
})
|
||||
|
||||
it('should support create new and open a modal', () => {
|
||||
let activeInstances: NgbModalRef[]
|
||||
modalService.activeInstances.subscribe((v) => (activeInstances = v))
|
||||
component.createTag('foo')
|
||||
expect(modalService.hasOpenModals()).toBeTruthy()
|
||||
expect(activeInstances[0].componentInstance.object.name).toEqual('foo')
|
||||
})
|
||||
|
||||
it('should support create new using last search term and open a modal', () => {
|
||||
let activeInstances: NgbModalRef[]
|
||||
modalService.activeInstances.subscribe((v) => (activeInstances = v))
|
||||
component.select.searchTerm = 'foobar'
|
||||
component.createTag()
|
||||
expect(modalService.hasOpenModals()).toBeTruthy()
|
||||
expect(activeInstances[0].componentInstance.object.name).toEqual('foobar')
|
||||
const editDialog = activeInstances[0]
|
||||
.componentInstance as TagEditDialogComponent
|
||||
editDialog.save() // create is mocked
|
||||
fixture.detectChanges()
|
||||
fixture.whenStable().then(() => {
|
||||
expect(fixture.debugElement.nativeElement.textContent).toContain('foobar')
|
||||
})
|
||||
})
|
||||
|
||||
it('support remove tags', () => {
|
||||
component.tags = tags
|
||||
component.value = [1, 2]
|
||||
component.removeTag(new PointerEvent('point'), 2)
|
||||
expect(component.value).toEqual([1])
|
||||
|
||||
component.disabled = true
|
||||
component.removeTag(new PointerEvent('point'), 1)
|
||||
expect(component.value).toEqual([1])
|
||||
})
|
||||
|
||||
it('should get tags', () => {
|
||||
component.tags = null
|
||||
expect(component.getTag(2)).toBeNull()
|
||||
component.tags = tags
|
||||
expect(component.getTag(2)).toEqual(tags[1])
|
||||
expect(component.getTag(4)).toBeUndefined()
|
||||
})
|
||||
})
|
@@ -5,12 +5,16 @@ import {
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from '@angular/core'
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||
import { TagEditDialogComponent } from '../../edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
||||
import { TagService } from 'src/app/services/rest/tag.service'
|
||||
import { EditDialogMode } from '../../edit-dialog/edit-dialog.component'
|
||||
import { first, firstValueFrom, tap } from 'rxjs'
|
||||
import { NgSelectComponent } from '@ng-select/ng-select'
|
||||
|
||||
@Component({
|
||||
providers: [
|
||||
@@ -25,7 +29,10 @@ import { TagService } from 'src/app/services/rest/tag.service'
|
||||
styleUrls: ['./tags.component.scss'],
|
||||
})
|
||||
export class TagsComponent implements OnInit, ControlValueAccessor {
|
||||
constructor(private tagService: TagService, private modalService: NgbModal) {
|
||||
constructor(
|
||||
private tagService: TagService,
|
||||
private modalService: NgbModal
|
||||
) {
|
||||
this.createTagRef = this.createTag.bind(this)
|
||||
}
|
||||
|
||||
@@ -70,14 +77,14 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
|
||||
@Output()
|
||||
filterDocuments = new EventEmitter<PaperlessTag[]>()
|
||||
|
||||
value: number[]
|
||||
@ViewChild('tagSelect') select: NgSelectComponent
|
||||
|
||||
tags: PaperlessTag[]
|
||||
value: number[] = []
|
||||
|
||||
tags: PaperlessTag[] = []
|
||||
|
||||
public createTagRef: (name) => void
|
||||
|
||||
private _lastSearchTerm: string
|
||||
|
||||
getTag(id: number) {
|
||||
if (this.tags) {
|
||||
return this.tags.find((tag) => tag.id == id)
|
||||
@@ -105,17 +112,22 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
|
||||
var modal = this.modalService.open(TagEditDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.dialogMode = 'create'
|
||||
modal.componentInstance.dialogMode = EditDialogMode.CREATE
|
||||
if (name) modal.componentInstance.object = { name: name }
|
||||
else if (this._lastSearchTerm)
|
||||
modal.componentInstance.object = { name: this._lastSearchTerm }
|
||||
modal.componentInstance.succeeded.subscribe((newTag) => {
|
||||
this.tagService.listAll().subscribe((tags) => {
|
||||
this.tags = tags.results
|
||||
this.value = [...this.value, newTag.id]
|
||||
this.onChange(this.value)
|
||||
})
|
||||
})
|
||||
else if (this.select.searchTerm)
|
||||
modal.componentInstance.object = { name: this.select.searchTerm }
|
||||
this.select.searchTerm = null
|
||||
this.select.detectChanges()
|
||||
return firstValueFrom(
|
||||
(modal.componentInstance as TagEditDialogComponent).succeeded.pipe(
|
||||
first(),
|
||||
tap(() => {
|
||||
this.tagService.listAll().subscribe((tags) => {
|
||||
this.tags = tags.results
|
||||
})
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
getSuggestions() {
|
||||
@@ -133,20 +145,6 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
|
||||
this.onChange(this.value)
|
||||
}
|
||||
|
||||
clearLastSearchTerm() {
|
||||
this._lastSearchTerm = null
|
||||
}
|
||||
|
||||
onSearch($event) {
|
||||
this._lastSearchTerm = $event.term
|
||||
}
|
||||
|
||||
onBlur() {
|
||||
setTimeout(() => {
|
||||
this.clearLastSearchTerm()
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
get hasPrivate(): boolean {
|
||||
return this.value.some(
|
||||
(t) => this.tags?.find((t2) => t2.id === t) === undefined
|
||||
|
@@ -0,0 +1,36 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import {
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
} from '@angular/forms'
|
||||
import { TextComponent } from './text.component'
|
||||
|
||||
describe('TextComponent', () => {
|
||||
let component: TextComponent
|
||||
let fixture: ComponentFixture<TextComponent>
|
||||
let input: HTMLInputElement
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [TextComponent],
|
||||
providers: [],
|
||||
imports: [FormsModule, ReactiveFormsModule],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(TextComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
input = component.inputField.nativeElement
|
||||
})
|
||||
|
||||
it('should support use of input field', () => {
|
||||
expect(component.value).toBeUndefined()
|
||||
// TODO: why doesnt this work?
|
||||
// input.value = 'foo'
|
||||
// input.dispatchEvent(new Event('change'))
|
||||
// fixture.detectChanges()
|
||||
// expect(component.value).toEqual('foo')
|
||||
})
|
||||
})
|
18
src-ui/src/app/components/common/logo/logo.component.html
Normal file
18
src-ui/src/app/components/common/logo/logo.component.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<svg [class]="getClasses()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2897.4 896.6" [attr.height]="height">
|
||||
<path class="leaf" d="M140,713.7c-3.4-16.4-10.3-49.1-11.2-49.1c-145.7-87.1-128.4-238-80.2-324.2C59,449,251.2,524,139.1,656.8 c-0.9,1.7,5.2,22.4,10.3,41.4c22.4-37.9,56-83.6,54.3-87.9C65.9,273.9,496.9,248.1,586.6,39.4c40.5,201.8-20.7,513.9-367.2,593.2 c-1.7,0.9-62.9,108.6-65.5,109.5c0-1.7-25.9-0.9-22.4-9.5C133.1,727.4,136.6,720.6,140,713.7L140,713.7z M135.7,632.6 c44-50.9-7.8-137.9-38.8-166.4C149.5,556.7,146,609.3,135.7,632.6L135.7,632.6z" transform="translate(0)" style="fill:#17541f"/>
|
||||
<g class="text" style="fill:#000">
|
||||
<path d="M1022.3,428.7c-17.8-19.9-42.7-29.8-74.7-29.8c-22.3,0-42.4,5.7-60.5,17.3c-18.1,11.6-32.3,27.5-42.5,47.8 s-15.3,42.9-15.3,67.8c0,24.9,5.1,47.5,15.3,67.8c10.3,20.3,24.4,36.2,42.5,47.8c18.1,11.5,38.3,17.3,60.5,17.3 c32,0,56.9-9.9,74.7-29.8v20.4v0.2h84.5V408.3h-84.5V428.7z M1010.5,575c-10.2,11.7-23.6,17.6-40.2,17.6s-29.9-5.9-40-17.6 s-15.1-26.1-15.1-43.3c0-17.1,5-31.6,15.1-43.3s23.4-17.6,40-17.6c16.6,0,30,5.9,40.2,17.6s15.3,26.1,15.3,43.3 S1020.7,563.3,1010.5,575z" transform="translate(0)"/>
|
||||
<path d="M1381,416.1c-18.1-11.5-38.3-17.3-60.5-17.4c-32,0-56.9,9.9-74.7,29.8v-20.4h-84.5v390.7h84.5v-164 c17.8,19.9,42.7,29.8,74.7,29.8c22.3,0,42.4-5.7,60.5-17.3s32.3-27.5,42.5-47.8c10.2-20.3,15.3-42.9,15.3-67.8s-5.1-47.5-15.3-67.8 C1413.2,443.6,1399.1,427.7,1381,416.1z M1337.9,575c-10.1,11.7-23.4,17.6-40,17.6s-29.9-5.9-40-17.6s-15.1-26.1-15.1-43.3 c0-17.1,5-31.6,15.1-43.3s23.4-17.6,40-17.6s29.9,5.9,40,17.6s15.1,26.1,15.1,43.3S1347.9,563.3,1337.9,575z" transform="translate(0)"/>
|
||||
<path d="M1672.2,416.8c-20.5-12-43-18-67.6-18c-24.9,0-47.6,5.9-68,17.6c-20.4,11.7-36.5,27.7-48.2,48s-17.6,42.7-17.6,67.3 c0.3,25.2,6.2,47.8,17.8,68c11.5,20.2,28,36,49.3,47.6c21.3,11.5,45.9,17.3,73.8,17.3c48.6,0,86.8-14.7,114.7-44l-52.5-48.9 c-8.6,8.3-17.6,14.6-26.7,19c-9.3,4.3-21.1,6.4-35.3,6.4c-11.6,0-22.5-3.6-32.7-10.9c-10.3-7.3-17.1-16.5-20.7-27.8h180l0.4-11.6 c0-29.6-6-55.7-18-78.2S1692.6,428.8,1672.2,416.8z M1558.3,503.2c2.1-12.1,7.5-21.8,16.2-29.1s18.7-10.9,30-10.9 s21.2,3.6,29.8,10.9c8.6,7.2,13.9,16.9,16,29.1H1558.3z" transform="translate(0)"/>
|
||||
<path d="M1895.3,411.7c-11,5.6-20.3,13.7-28,24.4h-0.1v-28h-84.5v247.3h84.5V536.3c0-22.6,4.7-38.1,14.2-46.5 c9.5-8.5,22.7-12.7,39.6-12.7c6.2,0,13.5,1,21.8,3.1l10.7-72c-5.9-3.3-14.5-4.9-25.8-4.9C1917.1,403.3,1906.3,406.1,1895.3,411.7z" transform="translate(0)"/>
|
||||
<rect x="1985" y="277.4" width="84.5" height="377.8" transform="translate(0)"/>
|
||||
<path d="M2313.2,416.8c-20.5-12-43-18-67.6-18c-24.9,0-47.6,5.9-68,17.6s-36.5,27.7-48.2,48c-11.7,20.3-17.6,42.7-17.6,67.3 c0.3,25.2,6.2,47.8,17.8,68c11.5,20.2,28,36,49.3,47.6c21.3,11.5,45.9,17.3,73.8,17.3c48.6,0,86.8-14.7,114.7-44l-52.5-48.9 c-8.6,8.3-17.6,14.6-26.7,19c-9.3,4.3-21.1,6.4-35.3,6.4c-11.6,0-22.5-3.6-32.7-10.9c-10.3-7.3-17.1-16.5-20.7-27.8h180l0.4-11.6 c0-29.6-6-55.7-18-78.2S2333.6,428.8,2313.2,416.8z M2199.3,503.2c2.1-12.1,7.5-21.8,16.2-29.1s18.7-10.9,30-10.9 s21.2,3.6,29.8,10.9c8.6,7.2,13.9,16.9,16,29.1H2199.3z" transform="translate(0)"/>
|
||||
<path d="M2583.6,507.7c-13.8-4.4-30.6-8.1-50.5-11.1c-15.1-2.7-26.1-5.2-32.9-7.6c-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7,6.7-10.9 c4.4-2.2,11.5-3.3,21.3-3.3c11.6,0,24.3,2.4,38.1,7.2c13.9,4.8,26.2,11,36.9,18.4l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8 c-18.7-7.1-39.6-10.7-62.7-10.7c-33.7,0-60.2,7.6-79.3,22.7c-19.1,15.1-28.7,36.1-28.7,63.1c0,19,4.8,33.9,14.4,44.7 c9.6,10.8,21,18.5,34,22.9c13.1,4.5,28.9,8.3,47.6,11.6c14.6,2.7,25.1,5.3,31.6,7.8s9.8,6.5,9.8,11.8c0,10.4-9.7,15.6-29.3,15.6 c-13.7,0-28.5-2.3-44.7-6.9c-16.1-4.6-29.2-11.3-39.3-20.2l-33.3,60c9.2,7.4,24.6,14.7,46.2,22c21.7,7.3,45.2,10.9,70.7,10.9 c34.7,0,62.9-7.4,84.5-22.4c21.7-15,32.5-37.3,32.5-66.9c0-19.3-5-34.2-15.1-44.9S2597.4,512.1,2583.6,507.7z" transform="translate(0)"/>
|
||||
<path d="M2883.4,575.3c0-19.3-5-34.2-15.1-44.9s-22-18.3-35.8-22.7c-13.8-4.4-30.6-8.1-50.5-11.1c-15.1-2.7-26.1-5.2-32.9-7.6 c-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7,6.7-10.9c4.4-2.2,11.5-3.3,21.3-3.3c11.6,0,24.3,2.4,38.1,7.2c13.9,4.8,26.2,11,36.9,18.4 l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8c-18.7-7.1-39.6-10.7-62.7-10.7c-33.7,0-60.2,7.6-79.3,22.7 c-19.1,15.1-28.7,36.1-28.7,63.1c0,19,4.8,33.9,14.4,44.7c9.6,10.8,21,18.5,34,22.9c13.1,4.5,28.9,8.3,47.6,11.6 c14.6,2.7,25.1,5.3,31.6,7.8s9.8,6.5,9.8,11.8c0,10.4-9.7,15.6-29.3,15.6c-13.7,0-28.5-2.3-44.7-6.9c-16.1-4.6-29.2-11.3-39.3-20.2 l-33.3,60c9.2,7.4,24.6,14.7,46.2,22c21.7,7.3,45.2,10.9,70.7,10.9c34.7,0,62.9-7.4,84.5-22.4 C2872.6,627.2,2883.4,604.9,2883.4,575.3z" transform="translate(0)"/>
|
||||
<rect x="2460.7" y="738.7" width="59.6" height="17.2" transform="translate(0)"/>
|
||||
<path d="M2596.5,706.4c-5.7,0-11,1-15.8,3s-9,5-12.5,8.9v-9.4h-19.4v93.6h19.4v-52c0-8.6,2.1-15.3,6.3-20c4.2-4.7,9.5-7.1,15.9-7.1 c7.8,0,13.4,2.3,16.8,6.7c3.4,4.5,5.1,11.3,5.1,20.5v52h19.4v-56.8c0-12.8-3.2-22.6-9.5-29.3 C2615.8,709.8,2607.3,706.4,2596.5,706.4z" transform="translate(0)"/>
|
||||
<path d="M2733.8,717.7c-3.6-3.4-7.9-6.1-13.1-8.2s-10.6-3.1-16.2-3.1c-8.7,0-16.5,2.1-23.5,6.3s-12.5,10-16.5,17.3 c-4,7.3-6,15.4-6,24.4c0,8.9,2,17.1,6,24.3c4,7.3,9.5,13,16.5,17.2s14.9,6.3,23.5,6.3c5.6,0,11-1,16.2-3.1 c5.1-2.1,9.5-4.8,13.1-8.2v24.4c0,8.5-2.5,14.8-7.6,18.7c-5,3.9-11,5.9-18,5.9c-6.7,0-12.4-1.6-17.3-4.7c-4.8-3.1-7.6-7.7-8.3-13.8 h-19.4c0.6,7.7,2.9,14.2,7.1,19.5s9.6,9.3,16.2,12c6.6,2.7,13.8,4,21.7,4c12.8,0,23.5-3.4,32-10.1c8.6-6.7,12.8-17.1,12.8-31.1 V708.9h-19.2V717.7z M2732.2,770.1c-2.5,4.7-6,8.3-10.4,11.2c-4.4,2.7-9.4,4-14.9,4c-5.7,0-10.8-1.4-15.2-4.3s-7.8-6.7-10.2-11.4 c-2.3-4.8-3.5-9.8-3.5-15.2c0-5.5,1.1-10.6,3.5-15.3s5.8-8.5,10.2-11.3s9.5-4.2,15.2-4.2c5.5,0,10.5,1.4,14.9,4s7.9,6.3,10.4,11 s3.8,10,3.8,15.8S2734.7,765.4,2732.2,770.1z" transform="translate(0)"/>
|
||||
<polygon points="2867.9,708.9 2846.5,708.9 2820.9,741.9 2795.5,708.9 2773.1,708.9 2809.1,755 2771.5,802.5 2792.9,802.5 2820.1,767.9 2847.2,802.6 2869.6,802.6 2832,754.4 " transform="translate(0)"/>
|
||||
<path d="M757.6,293.7c-20-10.8-42.6-16.2-67.8-16.2H600c-8.5,39.2-21.1,76.4-37.6,111.3c-9.9,20.8-21.1,40.6-33.6,59.4v207.2h88.9 V521.5h72c25.2,0,47.8-5.4,67.8-16.2s35.7-25.6,47.1-44.2c11.4-18.7,17.1-39.1,17.1-61.3c0.1-22.7-5.6-43.3-17-61.9 C793.3,319.2,777.6,304.5,757.6,293.7z M716.6,434.3c-9.3,8.9-21.6,13.3-36.7,13.3l-62.2,0.4v-92.5l62.2-0.4 c15.1,0,27.3,4.4,36.7,13.3c9.4,8.9,14,19.9,14,32.9C730.6,414.5,726,425.4,716.6,434.3z" transform="translate(0)"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 6.3 KiB |
36
src-ui/src/app/components/common/logo/logo.component.spec.ts
Normal file
36
src-ui/src/app/components/common/logo/logo.component.spec.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
|
||||
import { LogoComponent } from './logo.component'
|
||||
import { By } from '@angular/platform-browser'
|
||||
|
||||
describe('LogoComponent', () => {
|
||||
let component: LogoComponent
|
||||
let fixture: ComponentFixture<LogoComponent>
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [LogoComponent],
|
||||
})
|
||||
fixture = TestBed.createComponent(LogoComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support extra classes', () => {
|
||||
expect(fixture.debugElement.queryAll(By.css('.foo'))).toHaveLength(0)
|
||||
component.extra_classes = 'foo'
|
||||
fixture.detectChanges()
|
||||
expect(fixture.debugElement.queryAll(By.css('.foo'))).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should support setting height', () => {
|
||||
expect(fixture.debugElement.query(By.css('svg')).attributes.height).toEqual(
|
||||
'6em'
|
||||
)
|
||||
component.height = '10em'
|
||||
fixture.detectChanges()
|
||||
expect(fixture.debugElement.query(By.css('svg')).attributes.height).toEqual(
|
||||
'10em'
|
||||
)
|
||||
})
|
||||
})
|
18
src-ui/src/app/components/common/logo/logo.component.ts
Normal file
18
src-ui/src/app/components/common/logo/logo.component.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'app-logo',
|
||||
templateUrl: './logo.component.html',
|
||||
styleUrls: ['./logo.component.scss'],
|
||||
})
|
||||
export class LogoComponent {
|
||||
@Input()
|
||||
extra_classes: string
|
||||
|
||||
@Input()
|
||||
height = '6em'
|
||||
|
||||
getClasses() {
|
||||
return ['logo'].concat(this.extra_classes).join(' ')
|
||||
}
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { Title } from '@angular/platform-browser'
|
||||
import { PageHeaderComponent } from './page-header.component'
|
||||
import { environment } from 'src/environments/environment'
|
||||
|
||||
describe('PageHeaderComponent', () => {
|
||||
let component: PageHeaderComponent
|
||||
let fixture: ComponentFixture<PageHeaderComponent>
|
||||
let titleService: Title
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [PageHeaderComponent],
|
||||
providers: [],
|
||||
imports: [],
|
||||
}).compileComponents()
|
||||
|
||||
titleService = TestBed.inject(Title)
|
||||
fixture = TestBed.createComponent(PageHeaderComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should display title + subtitle', () => {
|
||||
component.title = 'Foo'
|
||||
component.subTitle = 'Bar'
|
||||
fixture.detectChanges()
|
||||
expect(fixture.nativeElement.textContent).toContain('FooBar')
|
||||
})
|
||||
|
||||
it('should set html title', () => {
|
||||
const titleSpy = jest.spyOn(titleService, 'setTitle')
|
||||
component.title = 'Foo Bar'
|
||||
expect(titleSpy).toHaveBeenCalledWith(`Foo Bar - ${environment.appTitle}`)
|
||||
})
|
||||
})
|
@@ -0,0 +1,90 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { PermissionsDialogComponent } from './permissions-dialog.component'
|
||||
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { of } from 'rxjs'
|
||||
import { PermissionsFormComponent } from '../input/permissions/permissions-form/permissions-form.component'
|
||||
import { SelectComponent } from '../input/select/select.component'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { PermissionsUserComponent } from '../input/permissions/permissions-user/permissions-user.component'
|
||||
import { PermissionsGroupComponent } from '../input/permissions/permissions-group/permissions-group.component'
|
||||
|
||||
const set_permissions = {
|
||||
owner: 10,
|
||||
set_permissions: {
|
||||
view: {
|
||||
users: [1],
|
||||
groups: [],
|
||||
},
|
||||
edit: {
|
||||
users: [1],
|
||||
groups: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
describe('PermissionsDialogComponent', () => {
|
||||
let component: PermissionsDialogComponent
|
||||
let fixture: ComponentFixture<PermissionsDialogComponent>
|
||||
let modal: NgbActiveModal
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
PermissionsDialogComponent,
|
||||
SafeHtmlPipe,
|
||||
SelectComponent,
|
||||
PermissionsFormComponent,
|
||||
PermissionsUserComponent,
|
||||
PermissionsGroupComponent,
|
||||
],
|
||||
providers: [
|
||||
NgbActiveModal,
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {
|
||||
listAll: () =>
|
||||
of({
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
username: 'user1',
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
username: 'user10',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
NgSelectModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
modal = TestBed.inject(NgbActiveModal)
|
||||
fixture = TestBed.createComponent(PermissionsDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should return permissions', () => {
|
||||
component.form.get('permissions_form').setValue(set_permissions)
|
||||
expect(component.permissions).toEqual(set_permissions)
|
||||
})
|
||||
|
||||
it('should close modal on cancel', () => {
|
||||
const closeSpy = jest.spyOn(modal, 'close')
|
||||
component.cancelClicked()
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
@@ -0,0 +1,157 @@
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { of } from 'rxjs'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
import {
|
||||
OwnerFilterType,
|
||||
PermissionsFilterDropdownComponent,
|
||||
PermissionsSelectionModel,
|
||||
} from './permissions-filter-dropdown.component'
|
||||
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
|
||||
const currentUserID = 13
|
||||
|
||||
describe('PermissionsFilterDropdownComponent', () => {
|
||||
let component: PermissionsFilterDropdownComponent
|
||||
let fixture: ComponentFixture<PermissionsFilterDropdownComponent>
|
||||
let ownerFilterSetResult: PermissionsSelectionModel
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
PermissionsFilterDropdownComponent,
|
||||
ClearableBadgeComponent,
|
||||
IfPermissionsDirective,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {
|
||||
listAll: () =>
|
||||
of({
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
username: 'user1',
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
username: 'user10',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: PermissionsService,
|
||||
useValue: {
|
||||
currentUserCan: () => true,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: SettingsService,
|
||||
useValue: {
|
||||
currentUser: {
|
||||
id: currentUserID,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
NgSelectModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(PermissionsFilterDropdownComponent)
|
||||
component = fixture.componentInstance
|
||||
component.ownerFilterSet.subscribe(
|
||||
(model) => (ownerFilterSetResult = model)
|
||||
)
|
||||
component.selectionModel = new PermissionsSelectionModel()
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should report is active', () => {
|
||||
component.setFilter(OwnerFilterType.NONE)
|
||||
expect(component.isActive).toBeFalsy()
|
||||
component.setFilter(OwnerFilterType.OTHERS)
|
||||
expect(component.isActive).toBeTruthy()
|
||||
component.setFilter(OwnerFilterType.NONE)
|
||||
component.selectionModel.hideUnowned = true
|
||||
expect(component.isActive).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should support reset', () => {
|
||||
component.setFilter(OwnerFilterType.OTHERS)
|
||||
expect(component.selectionModel.ownerFilter).not.toEqual(
|
||||
OwnerFilterType.NONE
|
||||
)
|
||||
component.reset()
|
||||
expect(component.selectionModel.ownerFilter).toEqual(OwnerFilterType.NONE)
|
||||
})
|
||||
|
||||
it('should toggle owner filter type when users selected', () => {
|
||||
component.selectionModel.ownerFilter = OwnerFilterType.NONE
|
||||
|
||||
// this would normally be done by select component
|
||||
component.selectionModel.includeUsers = [12]
|
||||
component.onUserSelect()
|
||||
expect(component.selectionModel.ownerFilter).toEqual(OwnerFilterType.OTHERS)
|
||||
|
||||
// this would normally be done by select component
|
||||
component.selectionModel.includeUsers = null
|
||||
component.onUserSelect()
|
||||
|
||||
expect(component.selectionModel.ownerFilter).toEqual(OwnerFilterType.NONE)
|
||||
})
|
||||
it('should emit a selection model depending on the type of owner filter set', () => {
|
||||
component.selectionModel.ownerFilter = OwnerFilterType.NONE
|
||||
|
||||
component.setFilter(OwnerFilterType.SELF)
|
||||
expect(ownerFilterSetResult).toEqual({
|
||||
excludeUsers: [],
|
||||
hideUnowned: false,
|
||||
includeUsers: [],
|
||||
ownerFilter: OwnerFilterType.SELF,
|
||||
userID: currentUserID,
|
||||
})
|
||||
|
||||
component.setFilter(OwnerFilterType.NOT_SELF)
|
||||
expect(ownerFilterSetResult).toEqual({
|
||||
excludeUsers: [currentUserID],
|
||||
hideUnowned: false,
|
||||
includeUsers: [],
|
||||
ownerFilter: OwnerFilterType.NOT_SELF,
|
||||
userID: null,
|
||||
})
|
||||
|
||||
component.setFilter(OwnerFilterType.NONE)
|
||||
expect(ownerFilterSetResult).toEqual({
|
||||
excludeUsers: [],
|
||||
hideUnowned: false,
|
||||
includeUsers: [],
|
||||
ownerFilter: OwnerFilterType.NONE,
|
||||
userID: null,
|
||||
})
|
||||
|
||||
component.setFilter(OwnerFilterType.UNOWNED)
|
||||
expect(ownerFilterSetResult).toEqual({
|
||||
excludeUsers: [],
|
||||
hideUnowned: false,
|
||||
includeUsers: [],
|
||||
ownerFilter: OwnerFilterType.UNOWNED,
|
||||
userID: null,
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,96 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { PermissionsSelectComponent } from './permissions-select.component'
|
||||
import {
|
||||
FormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import {
|
||||
PermissionAction,
|
||||
PermissionType,
|
||||
} from 'src/app/services/permissions.service'
|
||||
import { By } from '@angular/platform-browser'
|
||||
|
||||
const permissions = [
|
||||
'add_document',
|
||||
'view_document',
|
||||
'change_document',
|
||||
'delete_document',
|
||||
'change_tag',
|
||||
'view_documenttype',
|
||||
]
|
||||
|
||||
const inheritedPermissions = ['change_tag', 'view_documenttype']
|
||||
|
||||
describe('PermissionsSelectComponent', () => {
|
||||
let component: PermissionsSelectComponent
|
||||
let fixture: ComponentFixture<PermissionsSelectComponent>
|
||||
let permissionsChangeResult: Permissions
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [PermissionsSelectComponent],
|
||||
providers: [],
|
||||
imports: [FormsModule, ReactiveFormsModule, NgbModule],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(PermissionsSelectComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
component.registerOnChange((r) => (permissionsChangeResult = r))
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should create controls for all PermissionType and PermissionAction', () => {
|
||||
expect(Object.values(component.form.controls)).toHaveLength(
|
||||
Object.keys(PermissionType).length
|
||||
)
|
||||
for (var type in component.form.controls) {
|
||||
expect(
|
||||
Object.values(component.form.controls[type].controls)
|
||||
).toHaveLength(Object.keys(PermissionAction).length)
|
||||
}
|
||||
// coverage
|
||||
component.registerOnTouched(() => {})
|
||||
component.setDisabledState(true)
|
||||
})
|
||||
|
||||
it('should allow toggle all on / off', () => {
|
||||
component.ngOnInit()
|
||||
expect(component.typesWithAllActions.values).toHaveLength(0)
|
||||
component.toggleAll({ target: { checked: true } }, 'Tag')
|
||||
expect(component.typesWithAllActions).toContain('Tag')
|
||||
component.toggleAll({ target: { checked: false } }, 'Tag')
|
||||
expect(component.typesWithAllActions.values).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should update on permissions set', () => {
|
||||
component.ngOnInit()
|
||||
component.writeValue(permissions)
|
||||
expect(permissionsChangeResult).toEqual(permissions)
|
||||
expect(component.typesWithAllActions).toContain('Document')
|
||||
})
|
||||
|
||||
it('should update checkboxes on permissions set', () => {
|
||||
component.ngOnInit()
|
||||
component.writeValue(permissions)
|
||||
fixture.detectChanges()
|
||||
const input1 = fixture.debugElement.query(By.css('input#Document_Add'))
|
||||
expect(input1.nativeElement.checked).toBeTruthy()
|
||||
const input2 = fixture.debugElement.query(By.css('input#Tag_Change'))
|
||||
expect(input2.nativeElement.checked).toBeTruthy()
|
||||
})
|
||||
|
||||
it('disable checkboxes when permissions are inherited', () => {
|
||||
component.ngOnInit()
|
||||
component.inheritedPermissions = inheritedPermissions
|
||||
expect(component.isInherited('Document', 'Add')).toBeFalsy()
|
||||
expect(component.isInherited('Document')).toBeFalsy()
|
||||
expect(component.isInherited('Tag', 'Change')).toBeTruthy()
|
||||
const input1 = fixture.debugElement.query(By.css('input#Document_Add'))
|
||||
expect(input1.nativeElement.disabled).toBeFalsy()
|
||||
const input2 = fixture.debugElement.query(By.css('input#Tag_Change'))
|
||||
expect(input2.nativeElement.disabled).toBeTruthy()
|
||||
})
|
||||
})
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user