diff --git a/.devcontainer/README.md b/.devcontainer/README.md index cec62c802..1b7429c8d 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -89,6 +89,18 @@ Additional tasks are available for common maintenance operations: - **Migrate Database**: To apply database migrations. - **Create Superuser**: To create an admin user for the application. +## Committing from the Host Machine + +The DevContainer automatically installs pre-commit hooks during setup. However, these hooks are configured for use inside the container. + +If you want to commit changes from your host machine (outside the DevContainer), you need to set up pre-commit on your host. This installs it as a standalone tool. + +```bash +uv tool install pre-commit && pre-commit install +``` + +After this, you can commit either from inside the DevContainer or from your host machine. + ## Let's Get Started! Follow the steps above to get your development environment up and running. Happy coding! diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index cec8e2177..aef29bd90 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,26 +3,30 @@ "dockerComposeFile": "docker-compose.devcontainer.sqlite-tika.yml", "service": "paperless-development", "workspaceFolder": "/usr/src/paperless/paperless-ngx", - "postCreateCommand": "/bin/bash -c 'rm -rf .venv/.* && uv sync --group dev && uv run pre-commit install'", + "containerEnv": { + "UV_CACHE_DIR": "/usr/src/paperless/paperless-ngx/.uv-cache" + }, + "postCreateCommand": "/bin/bash -c 'rm -rf .venv/.* && uv sync --group dev && uv run pre-commit install'", "customizations": { "vscode": { - "extensions": [ - "mhutchie.git-graph", - "ms-python.python", - "ms-vscode.js-debug-nightly", - "eamodio.gitlens", - "yzhang.markdown-all-in-one" - ], - "settings": { - "python.defaultInterpreterPath": "/usr/src/paperless/paperless-ngx/.venv/bin/python", - "python.pythonPath": "/usr/src/paperless/paperless-ngx/.venv/bin/python", - "python.terminal.activateEnvInCurrentTerminal": true, - "editor.formatOnPaste": false, - "editor.formatOnSave": true, - "editor.formatOnType": true, - "files.trimTrailingWhitespace": true - } + "extensions": [ + "mhutchie.git-graph", + "ms-python.python", + "ms-vscode.js-debug-nightly", + "eamodio.gitlens", + "yzhang.markdown-all-in-one", + "pnpm.pnpm" + ], + "settings": { + "python.defaultInterpreterPath": "/usr/src/paperless/paperless-ngx/.venv/bin/python", + "python.pythonPath": "/usr/src/paperless/paperless-ngx/.venv/bin/python", + "python.terminal.activateEnvInCurrentTerminal": true, + "editor.formatOnPaste": false, + "editor.formatOnSave": true, + "editor.formatOnType": true, + "files.trimTrailingWhitespace": true + } } - }, - "remoteUser": "paperless" - } + }, + "remoteUser": "paperless" +} diff --git a/.devcontainer/vscode/tasks.json b/.devcontainer/vscode/tasks.json index 6475e14d1..0cc954232 100644 --- a/.devcontainer/vscode/tasks.json +++ b/.devcontainer/vscode/tasks.json @@ -174,12 +174,22 @@ { "label": "Maintenance: Install Frontend Dependencies", "description": "Install frontend (pnpm) dependencies", - "type": "pnpm", - "script": "install", - "path": "src-ui", + "type": "shell", + "command": "pnpm install", "group": "clean", "problemMatcher": [], - "detail": "install dependencies from package" + "options": { + "cwd": "${workspaceFolder}/src-ui" + }, + "presentation": { + "echo": true, + "reveal": "always", + "focus": true, + "panel": "shared", + "showReuseMessage": false, + "clear": true, + "revealProblems": "onProblem" + } }, { "description": "Clean install frontend dependencies and build the frontend for production", diff --git a/.github/workflows/ci-backend.yml b/.github/workflows/ci-backend.yml index 98c10396c..a619f01c1 100644 --- a/.github/workflows/ci-backend.yml +++ b/.github/workflows/ci-backend.yml @@ -75,9 +75,6 @@ jobs: env: NLTK_DATA: ${{ env.NLTK_DATA }} PAPERLESS_CI_TEST: 1 - PAPERLESS_MAIL_TEST_HOST: ${{ secrets.TEST_MAIL_HOST }} - PAPERLESS_MAIL_TEST_USER: ${{ secrets.TEST_MAIL_USER }} - PAPERLESS_MAIL_TEST_PASSWD: ${{ secrets.TEST_MAIL_PASSWD }} run: | uv run \ --python ${{ steps.setup-python.outputs.python-version }} \ diff --git a/.gitignore b/.gitignore index c7b5c4d8e..715760b29 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ htmlcov/ .coverage .coverage.* .cache +.uv-cache nosetests.xml coverage.xml *,cover diff --git a/docker/compose/docker-compose.ci-test.yml b/docker/compose/docker-compose.ci-test.yml index d277406b8..f07f7fadb 100644 --- a/docker/compose/docker-compose.ci-test.yml +++ b/docker/compose/docker-compose.ci-test.yml @@ -23,3 +23,14 @@ services: container_name: tika network_mode: host restart: unless-stopped + greenmail: + image: greenmail/standalone:2.1.8 + hostname: greenmail + container_name: greenmail + environment: + # Enable only IMAP for now (SMTP available via 3025 if needed later) + GREENMAIL_OPTS: >- + -Dgreenmail.setup.test.imap -Dgreenmail.users=test@localhost:test -Dgreenmail.users.login=test@localhost -Dgreenmail.verbose + ports: + - "3143:3143" # IMAP + restart: unless-stopped diff --git a/docs/configuration.md b/docs/configuration.md index 41d43d424..ef252ad4a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1617,6 +1617,16 @@ processing. This only has an effect if Defaults to `0 1 * * *`, once per day. +## Share links + +#### [`PAPERLESS_SHARE_LINK_BUNDLE_CLEANUP_CRON=`](#PAPERLESS_SHARE_LINK_BUNDLE_CLEANUP_CRON) {#PAPERLESS_SHARE_LINK_BUNDLE_CLEANUP_CRON} + +: Controls how often Paperless-ngx removes expired share link bundles (and their generated ZIP archives). + +: If set to the string "disable", expired bundles are not cleaned up automatically. + + Defaults to `0 2 * * *`, once per day at 02:00. + ## Binaries There are a few external software packages that Paperless expects to diff --git a/docs/usage.md b/docs/usage.md index 7da83a3e1..f652164da 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -308,12 +308,14 @@ or using [email](#workflow-action-email) or [webhook](#workflow-action-webhook) ### Share Links -"Share links" are shareable public links to files and can be created and managed under the 'Send' button on the document detail screen. +"Share links" are public links to files (or an archive of files) and can be created and managed under the 'Send' button on the document detail screen or from the bulk editor. -- Share links do not require a user to login and thus link directly to a file. +- Share links do not require a user to login and thus link directly to a file or bundled download. - Links are unique and are of the form `{paperless-url}/share/{randomly-generated-slug}`. - Links can optionally have an expiration time set. - After a link expires or is deleted users will be redirected to the regular paperless-ngx login. +- From the document detail screen you can create a share link for that single document. +- From the bulk editor you can create a **share link bundle** for any selection. Paperless-ngx prepares a ZIP archive in the background and exposes a single share link. You can revisit the "Manage share link bundles" dialog to monitor progress, retry failed bundles, or delete links. !!! tip diff --git a/pyproject.toml b/pyproject.toml index 500461199..ac6c39b2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -114,15 +114,16 @@ testing = [ "daphne", "factory-boy~=3.3.1", "imagehash", - "pytest~=8.4.1", + "pytest~=9.0.0", "pytest-cov~=7.0.0", "pytest-django~=4.11.1", - "pytest-env", + "pytest-env~=1.2.0", "pytest-httpx", - "pytest-mock", - "pytest-rerunfailures", + "pytest-mock~=3.15.1", + #"pytest-randomly~=4.0.1", + "pytest-rerunfailures~=16.1", "pytest-sugar", - "pytest-xdist", + "pytest-xdist~=3.8.0", ] lint = [ @@ -260,11 +261,15 @@ write-changes = true ignore-words-list = "criterias,afterall,valeu,ureue,equest,ure,assertIn,Oktober,commitish" skip = "src-ui/src/locale/*,src-ui/pnpm-lock.yaml,src-ui/e2e/*,src/paperless_mail/tests/samples/*,src/documents/tests/samples/*,*.po,*.json" -[tool.pytest.ini_options] -minversion = "8.0" -pythonpath = [ - "src", -] +[tool.pytest] +minversion = "9.0" +pythonpath = [ "src" ] + +strict_config = true +strict_markers = true +strict_parametrization_ids = true +strict_xfail = true + testpaths = [ "src/documents/tests/", "src/paperless/tests/", @@ -275,6 +280,7 @@ testpaths = [ "src/paperless_remote/tests/", "src/paperless_ai/tests", ] + addopts = [ "--pythonwarnings=all", "--cov", @@ -282,11 +288,14 @@ addopts = [ "--cov-report=xml", "--numprocesses=auto", "--maxprocesses=16", - "--quiet", + "--dist=loadscope", "--durations=50", + "--durations-min=0.5", "--junitxml=junit.xml", - "-o junit_family=legacy", + "-o", + "junit_family=legacy", ] + norecursedirs = [ "src/locale/", ".venv/", "src-ui/" ] DJANGO_SETTINGS_MODULE = "paperless.settings" diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index ea44b87bf..88de81f58 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -314,6 +314,14 @@ src/app/app.component.ts 152 + + src/app/components/admin/settings/settings.component.html + 193 + + + src/app/components/admin/settings/settings.component.html + 197 + src/app/components/app-frame/app-frame.component.html 94 @@ -322,6 +330,14 @@ src/app/components/app-frame/app-frame.component.html 96 + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 85 + + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 36 + src/app/components/document-list/document-list.component.ts 192 @@ -534,7 +550,7 @@ src/app/components/document-detail/document-detail.component.html - 427 + 437 @@ -545,7 +561,7 @@ src/app/components/admin/settings/settings.component.html - 362 + 386 src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.html @@ -593,7 +609,7 @@ src/app/components/document-detail/document-detail.component.html - 420 + 430 src/app/components/document-list/bulk-editor/custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component.html @@ -761,7 +777,7 @@ src/app/components/document-detail/document-detail.component.html - 440 + 450 src/app/components/document-list/document-list.component.html @@ -914,88 +930,128 @@ 99,100 - - Items per page - - src/app/components/admin/settings/settings.component.html - 107 - - Sidebar src/app/components/admin/settings/settings.component.html - 123 + 107 Use 'slim' sidebar (icons only) src/app/components/admin/settings/settings.component.html - 127 + 111 Dark mode src/app/components/admin/settings/settings.component.html - 134 + 118 Use system settings src/app/components/admin/settings/settings.component.html - 137 + 121 Enable dark mode src/app/components/admin/settings/settings.component.html - 138 + 122 Invert thumbnails in dark mode src/app/components/admin/settings/settings.component.html - 139 + 123 Theme Color src/app/components/admin/settings/settings.component.html - 145 + 129 Reset src/app/components/admin/settings/settings.component.html - 152 + 136 + + + + Global search + + src/app/components/admin/settings/settings.component.html + 142 + + + src/app/components/app-frame/global-search/global-search.component.ts + 122 + + + + Do not include advanced search results + + src/app/components/admin/settings/settings.component.html + 145 + + + + Full search links to + + src/app/components/admin/settings/settings.component.html + 151 + + + + Title and content search + + src/app/components/admin/settings/settings.component.html + 155 + + + + Advanced search + + src/app/components/admin/settings/settings.component.html + 156 + + + src/app/components/app-frame/global-search/global-search.component.html + 24 + + + src/app/components/document-list/filter-editor/filter-editor.component.ts + 208 Update checking src/app/components/admin/settings/settings.component.html - 157 + 161 Enable update checking src/app/components/admin/settings/settings.component.html - 160 + 164 What's this? src/app/components/admin/settings/settings.component.html - 161 + 165 src/app/components/common/page-header/page-header.component.html @@ -1014,21 +1070,21 @@ Update checking works by pinging the public GitHub API for the latest release to determine whether a new version is available. Actual updating of the app must still be performed manually. src/app/components/admin/settings/settings.component.html - 165,167 + 169,171 No tracking data is collected by the app in any way. src/app/components/admin/settings/settings.component.html - 169 + 173 Saved Views src/app/components/admin/settings/settings.component.html - 175 + 179 src/app/components/app-frame/app-frame.component.html @@ -1047,152 +1103,126 @@ Show warning when closing saved views with unsaved changes src/app/components/admin/settings/settings.component.html - 178 + 182 Show document counts in sidebar saved views src/app/components/admin/settings/settings.component.html - 179 + 183 + + + + Items per page + + src/app/components/admin/settings/settings.component.html + 200 Document editing src/app/components/admin/settings/settings.component.html - 185 + 212 Use PDF viewer provided by the browser src/app/components/admin/settings/settings.component.html - 189 + 215 This is usually faster for displaying large PDF documents, but it might not work on some browsers. src/app/components/admin/settings/settings.component.html - 189 + 215 Default zoom src/app/components/admin/settings/settings.component.html - 195 + 221 Fit width src/app/components/admin/settings/settings.component.html - 199 + 225 Fit page src/app/components/admin/settings/settings.component.html - 200 + 226 Only applies to the Paperless-ngx PDF viewer. src/app/components/admin/settings/settings.component.html - 202 + 228 Automatically remove inbox tag(s) on save src/app/components/admin/settings/settings.component.html - 208 + 234 Show document thumbnail during loading src/app/components/admin/settings/settings.component.html - 214 + 240 - - Global search + + Built-in fields to show: src/app/components/admin/settings/settings.component.html - 218 - - - src/app/components/app-frame/global-search/global-search.component.ts - 122 + 246 - - Do not include advanced search results + + Uncheck fields to hide them on the document details page. src/app/components/admin/settings/settings.component.html - 221 - - - - Full search links to - - src/app/components/admin/settings/settings.component.html - 227 - - - - Title and content search - - src/app/components/admin/settings/settings.component.html - 231 - - - - Advanced search - - src/app/components/admin/settings/settings.component.html - 232 - - - src/app/components/app-frame/global-search/global-search.component.html - 24 - - - src/app/components/document-list/filter-editor/filter-editor.component.ts - 208 + 258 Bulk editing src/app/components/admin/settings/settings.component.html - 237 + 263 Show confirmation dialogs src/app/components/admin/settings/settings.component.html - 240 + 266 Apply on close src/app/components/admin/settings/settings.component.html - 241 + 267 Notes src/app/components/admin/settings/settings.component.html - 245 + 271 src/app/components/document-list/document-list.component.html @@ -1211,14 +1241,14 @@ Enable notes src/app/components/admin/settings/settings.component.html - 248 + 274 Permissions src/app/components/admin/settings/settings.component.html - 259 + 283 src/app/components/common/edit-dialog/group-edit-dialog/group-edit-dialog.component.html @@ -1234,7 +1264,7 @@ src/app/components/document-detail/document-detail.component.html - 365 + 375 src/app/components/document-list/bulk-editor/bulk-editor.component.html @@ -1281,28 +1311,28 @@ Default Permissions src/app/components/admin/settings/settings.component.html - 262 + 286 Settings apply to this user account for objects (Tags, Mail Rules, etc. but not documents) created via the web UI. src/app/components/admin/settings/settings.component.html - 266,268 + 290,292 Default Owner src/app/components/admin/settings/settings.component.html - 273 + 297 Objects without an owner can be viewed and edited by all users src/app/components/admin/settings/settings.component.html - 277 + 301 src/app/components/common/input/permissions/permissions-form/permissions-form.component.html @@ -1313,18 +1343,18 @@ Default View Permissions src/app/components/admin/settings/settings.component.html - 282 + 306 Users: src/app/components/admin/settings/settings.component.html - 287 + 311 src/app/components/admin/settings/settings.component.html - 314 + 338 src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html @@ -1355,11 +1385,11 @@ Groups: src/app/components/admin/settings/settings.component.html - 297 + 321 src/app/components/admin/settings/settings.component.html - 324 + 348 src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html @@ -1390,14 +1420,14 @@ Default Edit Permissions src/app/components/admin/settings/settings.component.html - 309 + 333 Edit permissions also grant viewing permissions src/app/components/admin/settings/settings.component.html - 333 + 357 src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html @@ -1416,7 +1446,7 @@ Notifications src/app/components/admin/settings/settings.component.html - 341 + 365 src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.html @@ -1427,49 +1457,49 @@ Document processing src/app/components/admin/settings/settings.component.html - 344 + 368 Show notifications when new documents are detected src/app/components/admin/settings/settings.component.html - 348 + 372 Show notifications when document processing completes successfully src/app/components/admin/settings/settings.component.html - 349 + 373 Show notifications when document processing fails src/app/components/admin/settings/settings.component.html - 350 + 374 Suppress notifications on dashboard src/app/components/admin/settings/settings.component.html - 351 + 375 This will suppress all messages about document processing status on the dashboard. src/app/components/admin/settings/settings.component.html - 351 + 375 Cancel src/app/components/admin/settings/settings.component.html - 361 + 385 src/app/components/common/confirm-dialog/confirm-dialog.component.ts @@ -1550,11 +1580,150 @@ 81 + + Archive serial number + + src/app/components/admin/settings/settings.component.ts + 95 + + + src/app/components/document-detail/document-detail.component.html + 150 + + + + Correspondent + + src/app/components/admin/settings/settings.component.ts + 97 + + + src/app/components/document-detail/document-detail.component.html + 155 + + + src/app/components/document-list/bulk-editor/bulk-editor.component.html + 19 + + + src/app/components/document-list/document-list.component.html + 211 + + + src/app/components/document-list/filter-editor/filter-editor.component.html + 50 + + + src/app/data/document.ts + 46 + + + src/app/data/document.ts + 89 + + + + Document type + + src/app/components/admin/settings/settings.component.ts + 98 + + + src/app/components/document-detail/document-detail.component.html + 159 + + + src/app/components/document-list/bulk-editor/bulk-editor.component.html + 33 + + + src/app/components/document-list/document-list.component.html + 251 + + + src/app/components/document-list/filter-editor/filter-editor.component.html + 61 + + + src/app/data/document.ts + 50 + + + src/app/data/document.ts + 91 + + + + Storage path + + src/app/components/admin/settings/settings.component.ts + 99 + + + src/app/components/document-detail/document-detail.component.html + 163 + + + src/app/components/document-list/bulk-editor/bulk-editor.component.html + 47 + + + src/app/components/document-list/document-list.component.html + 260 + + + src/app/components/document-list/filter-editor/filter-editor.component.html + 72 + + + src/app/data/document.ts + 54 + + + + Tags + + src/app/components/admin/settings/settings.component.ts + 100 + + + src/app/components/app-frame/app-frame.component.html + 188 + + + src/app/components/app-frame/app-frame.component.html + 191 + + + src/app/components/common/input/tags/tags.component.ts + 80 + + + src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html + 94 + + + src/app/components/document-list/bulk-editor/bulk-editor.component.html + 5 + + + src/app/components/document-list/document-list.component.html + 224 + + + src/app/components/document-list/filter-editor/filter-editor.component.html + 39 + + + src/app/data/document.ts + 42 + + Error retrieving users src/app/components/admin/settings/settings.component.ts - 226 + 248 src/app/components/admin/users-groups/users-groups.component.ts @@ -1565,7 +1734,7 @@ Error retrieving groups src/app/components/admin/settings/settings.component.ts - 245 + 267 src/app/components/admin/users-groups/users-groups.component.ts @@ -1576,28 +1745,28 @@ Settings were saved successfully. src/app/components/admin/settings/settings.component.ts - 548 + 577 Settings were saved successfully. Reload is required to apply some changes. src/app/components/admin/settings/settings.component.ts - 552 + 581 Reload now src/app/components/admin/settings/settings.component.ts - 553 + 582 An error occurred while saving settings. src/app/components/admin/settings/settings.component.ts - 563 + 592 src/app/components/app-frame/app-frame.component.ts @@ -1766,6 +1935,10 @@ src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts 94 + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 32 + src/app/components/document-list/document-list.component.html 269 @@ -1815,6 +1988,10 @@ src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html 67 + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 38 + src/app/components/document-detail/document-detail.component.html 50 @@ -2147,7 +2324,7 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.html - 145 + 167 src/app/components/manage/custom-fields/custom-fields.component.html @@ -2598,23 +2775,23 @@ src/app/components/document-detail/document-detail.component.ts - 1112 + 1121 src/app/components/document-detail/document-detail.component.ts - 1477 + 1486 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 798 + 802 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 831 + 835 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 850 + 854 src/app/components/manage/custom-fields/custom-fields.component.ts @@ -2787,41 +2964,6 @@ 107 - - Tags - - src/app/components/app-frame/app-frame.component.html - 188 - - - src/app/components/app-frame/app-frame.component.html - 191 - - - src/app/components/common/input/tags/tags.component.ts - 80 - - - src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html - 94 - - - src/app/components/document-list/bulk-editor/bulk-editor.component.html - 5 - - - src/app/components/document-list/document-list.component.html - 224 - - - src/app/components/document-list/filter-editor/filter-editor.component.html - 39 - - - src/app/data/document.ts - 42 - - Document Types @@ -3039,7 +3181,7 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.html - 117 + 139 src/app/components/document-list/document-card-large/document-card-large.component.html @@ -3228,31 +3370,31 @@ src/app/components/document-detail/document-detail.component.ts - 1065 + 1074 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 441 + 445 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 481 + 485 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 519 + 523 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 557 + 561 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 619 + 623 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 752 + 756 @@ -3333,7 +3475,7 @@ src/app/components/document-detail/document-detail.component.ts - 1528 + 1537 @@ -3344,7 +3486,7 @@ src/app/components/document-detail/document-detail.component.ts - 1529 + 1538 @@ -3355,7 +3497,7 @@ src/app/components/document-detail/document-detail.component.ts - 1530 + 1539 @@ -3477,6 +3619,10 @@ src/app/components/common/input/date/date.component.html 22 + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 155 + src/app/components/document-detail/document-detail.component.html 103 @@ -4465,7 +4611,7 @@ src/app/components/document-detail/document-detail.component.html - 331 + 341 @@ -4580,7 +4726,7 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.html - 101 + 124 @@ -6260,6 +6406,301 @@ 326 + + Selected documents: + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 10 + + + + + more… + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 22 + + + + Expires + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 31 + + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 87 + + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 35 + + + src/app/components/common/share-links-dialog/share-links-dialog.component.html + 52 + + + + Share archive version (if available) + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 47 + + + + Share link bundle requested + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 54 + + + + You can copy the share link below or open the manager to monitor progress. The link will start working once the bundle is ready. + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 55,57 + + + + Status + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 60 + + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 33 + + + src/app/components/common/system-status-dialog/system-status-dialog.component.html + 58 + + + src/app/components/common/toast/toast.component.html + 28 + + + src/app/components/manage/mail/mail.component.html + 114 + + + src/app/components/manage/mail/processed-mail-dialog/processed-mail-dialog.component.html + 35 + + + src/app/components/manage/workflows/workflows.component.html + 19 + + + + Slug + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 64 + + + + Link + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 66 + + + + Copy link + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 81 + + + + Never + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 93 + + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 101 + + + src/app/data/share-link.ts + 17 + + + + File version + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 96 + + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 37 + + + + Size + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 99 + + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 34 + + + + A zip file containing the selected documents will be created for this share link bundle. This process happens in the background and may take some time, especially for large bundles. + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 109 + + + + Manage share link bundles + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html + 113 + + + src/app/components/document-list/bulk-editor/bulk-editor.component.html + 119 + + + + Create share link bundle + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.ts + 61 + + + + Create link + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.ts + 62 + + + + Share link copied to clipboard. + + src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.ts + 96 + + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.ts + 112 + + + + Loading share link bundles… + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 10 + + + + Status updates every few seconds while bundles are being prepared. + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 21 + + + + No share link bundles currently exist. + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 25 + + + + Built: + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 48 + + + + View error details + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 62 + + + + Copy share link + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 113 + + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 122 + + + + Retry + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 132 + + + + Delete share link bundle + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html + 141 + + + + Share link bundles + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.ts + 42 + + + + Failed to load share link bundles. + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.ts + 66 + + + + Error retrieving share link bundles. + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.ts + 68 + + + + Share link bundle deleted. + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.ts + 121 + + + + Error deleting share link bundle. + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.ts + 127 + + + + Share link bundle rebuild requested. + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.ts + 139 + + + + Error requesting rebuild. + + src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.ts + 144 + + No existing links @@ -6281,50 +6722,11 @@ 48 - - Expires - - src/app/components/common/share-links-dialog/share-links-dialog.component.html - 52 - - - - 1 day - - src/app/components/common/share-links-dialog/share-links-dialog.component.ts - 25 - - - src/app/components/common/share-links-dialog/share-links-dialog.component.ts - 102 - - - - 7 days - - src/app/components/common/share-links-dialog/share-links-dialog.component.ts - 26 - - - - 30 days - - src/app/components/common/share-links-dialog/share-links-dialog.component.ts - 27 - - - - Never - - src/app/components/common/share-links-dialog/share-links-dialog.component.ts - 28 - - Share Links src/app/components/common/share-links-dialog/share-links-dialog.component.ts - 32 + 31 src/app/components/document-detail/document-detail.component.html @@ -6335,28 +6737,39 @@ Error retrieving links src/app/components/common/share-links-dialog/share-links-dialog.component.ts - 83 + 82 + + + + 1 day + + src/app/components/common/share-links-dialog/share-links-dialog.component.ts + 101 + + + src/app/data/share-link.ts + 14 days src/app/components/common/share-links-dialog/share-links-dialog.component.ts - 102 + 101 Error deleting link src/app/components/common/share-links-dialog/share-links-dialog.component.ts - 131 + 130 Error creating link src/app/components/common/share-links-dialog/share-links-dialog.component.ts - 159 + 158 @@ -6451,29 +6864,6 @@ 52 - - Status - - src/app/components/common/system-status-dialog/system-status-dialog.component.html - 58 - - - src/app/components/common/toast/toast.component.html - 28 - - - src/app/components/manage/mail/mail.component.html - 114 - - - src/app/components/manage/mail/processed-mail-dialog/processed-mail-dialog.component.html - 35 - - - src/app/components/manage/workflows/workflows.component.html - 19 - - Migration Status @@ -6853,7 +7243,7 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 386 + 390 this string is used to separate processing, failed and added on the file upload widget @@ -6991,7 +7381,7 @@ src/app/components/document-detail/document-detail.component.ts - 1476 + 1485 @@ -7007,6 +7397,10 @@ src/app/components/document-detail/document-detail.component.html 80 + + src/app/components/document-list/bulk-editor/bulk-editor.component.html + 111 + Previous @@ -7045,102 +7439,18 @@ 90 - - Archive serial number - - src/app/components/document-detail/document-detail.component.html - 149 - - Date created - - src/app/components/document-detail/document-detail.component.html - 150 - - - - Correspondent src/app/components/document-detail/document-detail.component.html 152 - - src/app/components/document-list/bulk-editor/bulk-editor.component.html - 19 - - - src/app/components/document-list/document-list.component.html - 211 - - - src/app/components/document-list/filter-editor/filter-editor.component.html - 50 - - - src/app/data/document.ts - 46 - - - src/app/data/document.ts - 89 - - - - Document type - - src/app/components/document-detail/document-detail.component.html - 154 - - - src/app/components/document-list/bulk-editor/bulk-editor.component.html - 33 - - - src/app/components/document-list/document-list.component.html - 251 - - - src/app/components/document-list/filter-editor/filter-editor.component.html - 61 - - - src/app/data/document.ts - 50 - - - src/app/data/document.ts - 91 - - - - Storage path - - src/app/components/document-detail/document-detail.component.html - 156 - - - src/app/components/document-list/bulk-editor/bulk-editor.component.html - 47 - - - src/app/components/document-list/document-list.component.html - 260 - - - src/app/components/document-list/filter-editor/filter-editor.component.html - 72 - - - src/app/data/document.ts - 54 - Default src/app/components/document-detail/document-detail.component.html - 157 + 164 src/app/components/manage/saved-views/saved-views.component.html @@ -7151,14 +7461,14 @@ Content src/app/components/document-detail/document-detail.component.html - 261 + 271 Metadata src/app/components/document-detail/document-detail.component.html - 270 + 280 src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts @@ -7169,196 +7479,196 @@ Date modified src/app/components/document-detail/document-detail.component.html - 277 + 287 Date added src/app/components/document-detail/document-detail.component.html - 281 + 291 Media filename src/app/components/document-detail/document-detail.component.html - 285 + 295 Original filename src/app/components/document-detail/document-detail.component.html - 289 + 299 Original MD5 checksum src/app/components/document-detail/document-detail.component.html - 293 + 303 Original file size src/app/components/document-detail/document-detail.component.html - 297 + 307 Original mime type src/app/components/document-detail/document-detail.component.html - 301 + 311 Archive MD5 checksum src/app/components/document-detail/document-detail.component.html - 306 + 316 Archive file size src/app/components/document-detail/document-detail.component.html - 312 + 322 Original document metadata src/app/components/document-detail/document-detail.component.html - 321 + 331 Archived document metadata src/app/components/document-detail/document-detail.component.html - 324 + 334 Notes src/app/components/document-detail/document-detail.component.html - 343,346 + 353,356 History src/app/components/document-detail/document-detail.component.html - 354 + 364 Duplicates src/app/components/document-detail/document-detail.component.html - 376,380 + 386,390 Duplicate documents detected: src/app/components/document-detail/document-detail.component.html - 382 + 392 In trash src/app/components/document-detail/document-detail.component.html - 393 + 403 Save & next src/app/components/document-detail/document-detail.component.html - 422 + 432 Save & close src/app/components/document-detail/document-detail.component.html - 425 + 435 Document loading... src/app/components/document-detail/document-detail.component.html - 435 + 445 Enter Password src/app/components/document-detail/document-detail.component.html - 489 + 499 An error occurred loading content: src/app/components/document-detail/document-detail.component.ts - 432,434 + 441,443 Document changes detected src/app/components/document-detail/document-detail.component.ts - 471 + 480 The version of this document in your browser session appears older than the existing version. src/app/components/document-detail/document-detail.component.ts - 472 + 481 Saving the document here may overwrite other changes that were made. To restore the existing version, discard your changes or close the document. src/app/components/document-detail/document-detail.component.ts - 473 + 482 Ok src/app/components/document-detail/document-detail.component.ts - 475 + 484 Next document src/app/components/document-detail/document-detail.component.ts - 601 + 610 Previous document src/app/components/document-detail/document-detail.component.ts - 611 + 620 Close document src/app/components/document-detail/document-detail.component.ts - 619 + 628 src/app/services/open-documents.service.ts @@ -7369,202 +7679,202 @@ Save document src/app/components/document-detail/document-detail.component.ts - 626 + 635 Save and close / next src/app/components/document-detail/document-detail.component.ts - 635 + 644 Error retrieving metadata src/app/components/document-detail/document-detail.component.ts - 690 + 699 Error retrieving suggestions. src/app/components/document-detail/document-detail.component.ts - 745 + 754 Document "" saved successfully. src/app/components/document-detail/document-detail.component.ts - 954 + 963 src/app/components/document-detail/document-detail.component.ts - 978 + 987 Error saving document "" src/app/components/document-detail/document-detail.component.ts - 984 + 993 Error saving document src/app/components/document-detail/document-detail.component.ts - 1034 + 1043 Do you really want to move the document "" to the trash? src/app/components/document-detail/document-detail.component.ts - 1066 + 1075 Documents can be restored prior to permanent deletion. src/app/components/document-detail/document-detail.component.ts - 1067 + 1076 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 754 + 758 Move to trash src/app/components/document-detail/document-detail.component.ts - 1069 + 1078 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 756 + 760 Error deleting document src/app/components/document-detail/document-detail.component.ts - 1088 + 1097 Reprocess confirm src/app/components/document-detail/document-detail.component.ts - 1108 + 1117 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 794 + 798 This operation will permanently recreate the archive file for this document. src/app/components/document-detail/document-detail.component.ts - 1109 + 1118 The archive file will be re-generated with the current settings. src/app/components/document-detail/document-detail.component.ts - 1110 + 1119 Reprocess operation for "" will begin in the background. Close and re-open or reload this document after the operation has completed to see new content. src/app/components/document-detail/document-detail.component.ts - 1120 + 1129 Error executing operation src/app/components/document-detail/document-detail.component.ts - 1131 + 1140 Error downloading document src/app/components/document-detail/document-detail.component.ts - 1180 + 1189 Page Fit src/app/components/document-detail/document-detail.component.ts - 1257 + 1266 PDF edit operation for "" will begin in the background. src/app/components/document-detail/document-detail.component.ts - 1495 + 1504 Error executing PDF edit operation src/app/components/document-detail/document-detail.component.ts - 1507 + 1516 Please enter the current password before attempting to remove it. src/app/components/document-detail/document-detail.component.ts - 1518 + 1527 Password removal operation for "" will begin in the background. src/app/components/document-detail/document-detail.component.ts - 1550 + 1559 Error executing password removal operation src/app/components/document-detail/document-detail.component.ts - 1564 + 1573 Print failed. src/app/components/document-detail/document-detail.component.ts - 1601 + 1610 Error loading document for printing. src/app/components/document-detail/document-detail.component.ts - 1613 + 1622 An error occurred loading tiff: src/app/components/document-detail/document-detail.component.ts - 1678 + 1687 src/app/components/document-detail/document-detail.component.ts - 1682 + 1691 @@ -7668,57 +7978,64 @@ 97 + + Create a share link bundle + + src/app/components/document-list/bulk-editor/bulk-editor.component.html + 116 + + Include: src/app/components/document-list/bulk-editor/bulk-editor.component.html - 123 + 145 Archived files src/app/components/document-list/bulk-editor/bulk-editor.component.html - 127 + 149 Original files src/app/components/document-list/bulk-editor/bulk-editor.component.html - 131 + 153 Use formatted filename src/app/components/document-list/bulk-editor/bulk-editor.component.html - 136 + 158 Error executing bulk operation src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 290 + 294 "" src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 378 + 382 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 384 + 388 "" and "" src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 380 + 384 This is for messages like 'modify "tag1" and "tag2"' @@ -7726,7 +8043,7 @@ and "" src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 388,390 + 392,394 this is for messages like 'modify "tag1", "tag2" and "tag3"' @@ -7734,14 +8051,14 @@ Confirm tags assignment src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 405 + 409 This operation will add the tag "" to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 411 + 415 @@ -7750,14 +8067,14 @@ )"/> to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 416,418 + 420,422 This operation will remove the tag "" from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 424 + 428 @@ -7766,7 +8083,7 @@ )"/> from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 429,431 + 433,435 @@ -7777,84 +8094,84 @@ )"/> on selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 433,437 + 437,441 Confirm correspondent assignment src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 474 + 478 This operation will assign the correspondent "" to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 476 + 480 This operation will remove the correspondent from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 478 + 482 Confirm document type assignment src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 512 + 516 This operation will assign the document type "" to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 514 + 518 This operation will remove the document type from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 516 + 520 Confirm storage path assignment src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 550 + 554 This operation will assign the storage path "" to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 552 + 556 This operation will remove the storage path from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 554 + 558 Confirm custom field assignment src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 583 + 587 This operation will assign the custom field "" to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 589 + 593 @@ -7863,14 +8180,14 @@ )"/> to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 594,596 + 598,600 This operation will remove the custom field "" from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 602 + 606 @@ -7879,7 +8196,7 @@ )"/> from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 607,609 + 611,613 @@ -7890,77 +8207,91 @@ )"/> on selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 611,615 + 615,619 Move selected document(s) to the trash? src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 753 + 757 This operation will permanently recreate the archive files for selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 795 + 799 The archive files will be re-generated with the current settings. src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 796 + 800 Rotate confirm src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 828 + 832 This operation will permanently rotate the original version of document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 829 + 833 Merge confirm src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 848 + 852 This operation will merge selected documents into a new document. src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 849 + 853 Merged document will be queued for consumption. src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 868 + 872 Custom fields updated. src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 892 + 896 Error updating custom fields. src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 901 + 905 + + + + Share link bundle creation requested. + + src/app/components/document-list/bulk-editor/bulk-editor.component.ts + 945 + + + + Share link bundle creation is not available yet. + + src/app/components/document-list/bulk-editor/bulk-editor.component.ts + 952 @@ -10118,6 +10449,62 @@ 321 + + Pending + + src/app/data/share-link-bundle.ts + 41 + + + + Processing + + src/app/data/share-link-bundle.ts + 42 + + + + Ready + + src/app/data/share-link-bundle.ts + 43 + + + + Failed + + src/app/data/share-link-bundle.ts + 44 + + + + Archive + + src/app/data/share-link-bundle.ts + 51 + + + + Original + + src/app/data/share-link-bundle.ts + 52 + + + + 7 days + + src/app/data/share-link.ts + 15 + + + + 30 days + + src/app/data/share-link.ts + 16 + + Warning: You have unsaved changes to your document(s). diff --git a/src-ui/src/app/components/admin/settings/settings.component.html b/src-ui/src/app/components/admin/settings/settings.component.html index b228dac32..807368aa6 100644 --- a/src-ui/src/app/components/admin/settings/settings.component.html +++ b/src-ui/src/app/components/admin/settings/settings.component.html @@ -103,22 +103,6 @@
-
- Items per page -
-
- - - -
-
- -
Sidebar
@@ -153,8 +137,28 @@
+ +
+
Global search
+
+
+ +
+
-
Update checking
+
+
+ Full search links to +
+
+ +
+
+ +
Update checking
@@ -179,11 +183,33 @@
-
-
-
Document editing
+
+ + + +
  • + Documents + +
    +
    +
    Documents
    +
    +
    + Items per page +
    +
    + +
    +
    + +
    Document editing
    @@ -209,31 +235,31 @@
    -
    +
    -
    Global search
    -
    -
    - -
    -
    -
    -
    - Full search links to -
    -
    - +
    +

    Built-in fields to show:

    + @for (option of documentDetailFieldOptions; track option.id) { +
    + + +
    + } +

    Uncheck fields to hide them on the document details page.

    - +
    +
    Bulk editing
    @@ -248,10 +274,8 @@
    -
    -
  • diff --git a/src-ui/src/app/components/admin/settings/settings.component.spec.ts b/src-ui/src/app/components/admin/settings/settings.component.spec.ts index 650d6d8ea..62a5aa363 100644 --- a/src-ui/src/app/components/admin/settings/settings.component.spec.ts +++ b/src-ui/src/app/components/admin/settings/settings.component.spec.ts @@ -201,9 +201,9 @@ describe('SettingsComponent', () => { const navigateSpy = jest.spyOn(router, 'navigate') const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink)) tabButtons[1].nativeElement.dispatchEvent(new MouseEvent('click')) - expect(navigateSpy).toHaveBeenCalledWith(['settings', 'permissions']) + expect(navigateSpy).toHaveBeenCalledWith(['settings', 'documents']) tabButtons[2].nativeElement.dispatchEvent(new MouseEvent('click')) - expect(navigateSpy).toHaveBeenCalledWith(['settings', 'notifications']) + expect(navigateSpy).toHaveBeenCalledWith(['settings', 'permissions']) const initSpy = jest.spyOn(component, 'initialize') component.isDirty = true // mock dirty @@ -213,8 +213,8 @@ describe('SettingsComponent', () => { expect(initSpy).not.toHaveBeenCalled() navigateSpy.mockResolvedValueOnce(true) // nav accepted even though dirty - tabButtons[1].nativeElement.dispatchEvent(new MouseEvent('click')) - expect(navigateSpy).toHaveBeenCalledWith(['settings', 'notifications']) + tabButtons[2].nativeElement.dispatchEvent(new MouseEvent('click')) + expect(navigateSpy).toHaveBeenCalledWith(['settings', 'permissions']) expect(initSpy).toHaveBeenCalled() }) @@ -226,7 +226,7 @@ describe('SettingsComponent', () => { activatedRoute.snapshot.fragment = '#notifications' const scrollSpy = jest.spyOn(viewportScroller, 'scrollToAnchor') component.ngOnInit() - expect(component.activeNavID).toEqual(3) // Notifications + expect(component.activeNavID).toEqual(4) // Notifications component.ngAfterViewInit() expect(scrollSpy).toHaveBeenCalledWith('#notifications') }) @@ -251,7 +251,7 @@ describe('SettingsComponent', () => { expect(toastErrorSpy).toHaveBeenCalled() expect(storeSpy).toHaveBeenCalled() expect(appearanceSettingsSpy).not.toHaveBeenCalled() - expect(setSpy).toHaveBeenCalledTimes(30) + expect(setSpy).toHaveBeenCalledTimes(31) // succeed storeSpy.mockReturnValueOnce(of(true)) @@ -366,4 +366,22 @@ describe('SettingsComponent', () => { settingsService.settingsSaved.emit(true) expect(maybeRefreshSpy).toHaveBeenCalled() }) + + it('should support toggling document detail fields', () => { + completeSetup() + const field = 'storage_path' + expect( + component.settingsForm.get('documentDetailsHiddenFields').value.length + ).toEqual(0) + component.toggleDocumentDetailField(field, false) + expect( + component.settingsForm.get('documentDetailsHiddenFields').value.length + ).toEqual(1) + expect(component.isDocumentDetailFieldShown(field)).toBeFalsy() + component.toggleDocumentDetailField(field, true) + expect( + component.settingsForm.get('documentDetailsHiddenFields').value.length + ).toEqual(0) + expect(component.isDocumentDetailFieldShown(field)).toBeTruthy() + }) }) diff --git a/src-ui/src/app/components/admin/settings/settings.component.ts b/src-ui/src/app/components/admin/settings/settings.component.ts index 614d2fcd0..990944ff6 100644 --- a/src-ui/src/app/components/admin/settings/settings.component.ts +++ b/src-ui/src/app/components/admin/settings/settings.component.ts @@ -70,9 +70,9 @@ import { ComponentWithPermissions } from '../../with-permissions/with-permission enum SettingsNavIDs { General = 1, - Permissions = 2, - Notifications = 3, - SavedViews = 4, + Documents = 2, + Permissions = 3, + Notifications = 4, } const systemLanguage = { code: '', name: $localize`Use system language` } @@ -81,6 +81,25 @@ const systemDateFormat = { name: $localize`Use date format of display language`, } +export enum DocumentDetailFieldID { + ArchiveSerialNumber = 'archive_serial_number', + Correspondent = 'correspondent', + DocumentType = 'document_type', + StoragePath = 'storage_path', + Tags = 'tags', +} + +const documentDetailFieldOptions = [ + { + id: DocumentDetailFieldID.ArchiveSerialNumber, + label: $localize`Archive serial number`, + }, + { id: DocumentDetailFieldID.Correspondent, label: $localize`Correspondent` }, + { id: DocumentDetailFieldID.DocumentType, label: $localize`Document type` }, + { id: DocumentDetailFieldID.StoragePath, label: $localize`Storage path` }, + { id: DocumentDetailFieldID.Tags, label: $localize`Tags` }, +] + @Component({ selector: 'pngx-settings', templateUrl: './settings.component.html', @@ -146,6 +165,7 @@ export class SettingsComponent pdfViewerDefaultZoom: new FormControl(null), documentEditingRemoveInboxTags: new FormControl(null), documentEditingOverlayThumbnail: new FormControl(null), + documentDetailsHiddenFields: new FormControl([]), searchDbOnly: new FormControl(null), searchLink: new FormControl(null), @@ -176,6 +196,8 @@ export class SettingsComponent public readonly ZoomSetting = ZoomSetting + public readonly documentDetailFieldOptions = documentDetailFieldOptions + get systemStatusHasErrors(): boolean { return ( this.systemStatus.database.status === SystemStatusItemStatus.ERROR || @@ -336,6 +358,9 @@ export class SettingsComponent documentEditingOverlayThumbnail: this.settings.get( SETTINGS_KEYS.DOCUMENT_EDITING_OVERLAY_THUMBNAIL ), + documentDetailsHiddenFields: this.settings.get( + SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS + ), searchDbOnly: this.settings.get(SETTINGS_KEYS.SEARCH_DB_ONLY), searchLink: this.settings.get(SETTINGS_KEYS.SEARCH_FULL_TYPE), } @@ -526,6 +551,10 @@ export class SettingsComponent SETTINGS_KEYS.DOCUMENT_EDITING_OVERLAY_THUMBNAIL, this.settingsForm.value.documentEditingOverlayThumbnail ) + this.settings.set( + SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS, + this.settingsForm.value.documentDetailsHiddenFields + ) this.settings.set( SETTINGS_KEYS.SEARCH_DB_ONLY, this.settingsForm.value.searchDbOnly @@ -587,6 +616,26 @@ export class SettingsComponent this.settingsForm.get('themeColor').patchValue('') } + isDocumentDetailFieldShown(fieldId: string): boolean { + const hiddenFields = + this.settingsForm.value.documentDetailsHiddenFields || [] + return !hiddenFields.includes(fieldId) + } + + toggleDocumentDetailField(fieldId: string, checked: boolean) { + const hiddenFields = new Set( + this.settingsForm.value.documentDetailsHiddenFields || [] + ) + if (checked) { + hiddenFields.delete(fieldId) + } else { + hiddenFields.add(fieldId) + } + this.settingsForm + .get('documentDetailsHiddenFields') + .setValue(Array.from(hiddenFields)) + } + showSystemStatus() { const modal: NgbModalRef = this.modalService.open( SystemStatusDialogComponent, diff --git a/src-ui/src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html b/src-ui/src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html new file mode 100644 index 000000000..b7fed28e1 --- /dev/null +++ b/src-ui/src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html @@ -0,0 +1,129 @@ + + + diff --git a/src-ui/src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.scss b/src-ui/src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src-ui/src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.spec.ts b/src-ui/src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.spec.ts new file mode 100644 index 000000000..da4d93c6a --- /dev/null +++ b/src-ui/src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.spec.ts @@ -0,0 +1,161 @@ +import { Clipboard } from '@angular/cdk/clipboard' +import { + ComponentFixture, + TestBed, + fakeAsync, + tick, +} from '@angular/core/testing' +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' +import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' +import { FileVersion } from 'src/app/data/share-link' +import { + ShareLinkBundleStatus, + ShareLinkBundleSummary, +} from 'src/app/data/share-link-bundle' +import { ToastService } from 'src/app/services/toast.service' +import { environment } from 'src/environments/environment' +import { ShareLinkBundleDialogComponent } from './share-link-bundle-dialog.component' + +class MockToastService { + showInfo = jest.fn() + showError = jest.fn() +} + +describe('ShareLinkBundleDialogComponent', () => { + let component: ShareLinkBundleDialogComponent + let fixture: ComponentFixture + let clipboard: Clipboard + let toastService: MockToastService + let activeModal: NgbActiveModal + let originalApiBaseUrl: string + + beforeEach(() => { + originalApiBaseUrl = environment.apiBaseUrl + toastService = new MockToastService() + + TestBed.configureTestingModule({ + imports: [ + ShareLinkBundleDialogComponent, + NgxBootstrapIconsModule.pick(allIcons), + ], + providers: [ + NgbActiveModal, + { provide: ToastService, useValue: toastService }, + ], + }) + + fixture = TestBed.createComponent(ShareLinkBundleDialogComponent) + component = fixture.componentInstance + clipboard = TestBed.inject(Clipboard) + activeModal = TestBed.inject(NgbActiveModal) + fixture.detectChanges() + }) + + afterEach(() => { + jest.clearAllTimers() + environment.apiBaseUrl = originalApiBaseUrl + }) + + it('builds payload and emits confirm on submit', () => { + const confirmSpy = jest.spyOn(component.confirmClicked, 'emit') + component.documents = [ + { id: 1, title: 'Doc 1' } as any, + { id: 2, title: 'Doc 2' } as any, + ] + component.form.setValue({ + shareArchiveVersion: false, + expirationDays: 3, + }) + + component.submit() + + expect(component.payload).toEqual({ + document_ids: [1, 2], + file_version: FileVersion.Original, + expiration_days: 3, + }) + expect(component.buttonsEnabled).toBe(false) + expect(confirmSpy).toHaveBeenCalled() + + component.form.setValue({ + shareArchiveVersion: true, + expirationDays: 7, + }) + component.submit() + + expect(component.payload).toEqual({ + document_ids: [1, 2], + file_version: FileVersion.Archive, + expiration_days: 7, + }) + }) + + it('ignores submit when bundle already created', () => { + component.createdBundle = { id: 1 } as ShareLinkBundleSummary + const confirmSpy = jest.spyOn(component, 'confirm') + component.submit() + expect(confirmSpy).not.toHaveBeenCalled() + }) + + it('limits preview to ten documents', () => { + const docs = Array.from({ length: 12 }).map((_, index) => ({ + id: index + 1, + })) + component.documents = docs as any + + expect(component.selectionCount).toBe(12) + expect(component.documentPreview).toHaveLength(10) + expect(component.documentPreview[0].id).toBe(1) + }) + + it('copies share link and resets state after timeout', fakeAsync(() => { + const copySpy = jest.spyOn(clipboard, 'copy').mockReturnValue(true) + const bundle = { + slug: 'bundle-slug', + status: ShareLinkBundleStatus.Ready, + } as ShareLinkBundleSummary + + component.copy(bundle) + + expect(copySpy).toHaveBeenCalledWith(component.getShareUrl(bundle)) + expect(component.copied).toBe(true) + expect(toastService.showInfo).toHaveBeenCalled() + + tick(3000) + expect(component.copied).toBe(false) + })) + + it('generates share URLs based on API base URL', () => { + environment.apiBaseUrl = 'https://example.com/api/' + expect( + component.getShareUrl({ slug: 'abc' } as ShareLinkBundleSummary) + ).toBe('https://example.com/share/abc') + }) + + it('opens manage dialog when callback provided', () => { + const manageSpy = jest.fn() + component.onOpenManage = manageSpy + component.openManage() + expect(manageSpy).toHaveBeenCalled() + }) + + it('falls back to cancel when manage callback missing', () => { + const cancelSpy = jest.spyOn(component, 'cancel') + component.onOpenManage = undefined + component.openManage() + expect(cancelSpy).toHaveBeenCalled() + }) + + it('maps status and file version labels', () => { + expect(component.statusLabel(ShareLinkBundleStatus.Processing)).toContain( + 'Processing' + ) + expect(component.fileVersionLabel(FileVersion.Archive)).toContain('Archive') + }) + + it('closes dialog when cancel invoked', () => { + const closeSpy = jest.spyOn(activeModal, 'close') + component.cancel() + expect(closeSpy).toHaveBeenCalled() + }) +}) diff --git a/src-ui/src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.ts b/src-ui/src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.ts new file mode 100644 index 000000000..37aa70950 --- /dev/null +++ b/src-ui/src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.ts @@ -0,0 +1,118 @@ +import { Clipboard } from '@angular/cdk/clipboard' +import { CommonModule } from '@angular/common' +import { Component, Input, inject } from '@angular/core' +import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms' +import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' +import { Document } from 'src/app/data/document' +import { + FileVersion, + SHARE_LINK_EXPIRATION_OPTIONS, +} from 'src/app/data/share-link' +import { + SHARE_LINK_BUNDLE_FILE_VERSION_LABELS, + SHARE_LINK_BUNDLE_STATUS_LABELS, + ShareLinkBundleCreatePayload, + ShareLinkBundleStatus, + ShareLinkBundleSummary, +} from 'src/app/data/share-link-bundle' +import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe' +import { FileSizePipe } from 'src/app/pipes/file-size.pipe' +import { ToastService } from 'src/app/services/toast.service' +import { environment } from 'src/environments/environment' +import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component' + +@Component({ + selector: 'pngx-share-link-bundle-dialog', + templateUrl: './share-link-bundle-dialog.component.html', + imports: [ + CommonModule, + ReactiveFormsModule, + NgxBootstrapIconsModule, + FileSizePipe, + DocumentTitlePipe, + ], + providers: [], +}) +export class ShareLinkBundleDialogComponent extends ConfirmDialogComponent { + private readonly formBuilder = inject(FormBuilder) + private readonly clipboard = inject(Clipboard) + private readonly toastService = inject(ToastService) + + private _documents: Document[] = [] + + selectionCount = 0 + documentPreview: Document[] = [] + form: FormGroup = this.formBuilder.group({ + shareArchiveVersion: true, + expirationDays: [7], + }) + payload: ShareLinkBundleCreatePayload | null = null + + readonly expirationOptions = SHARE_LINK_EXPIRATION_OPTIONS + + createdBundle: ShareLinkBundleSummary | null = null + copied = false + onOpenManage?: () => void + readonly statuses = ShareLinkBundleStatus + + constructor() { + super() + this.loading = false + this.title = $localize`Create share link bundle` + this.btnCaption = $localize`Create link` + } + + @Input() + set documents(docs: Document[]) { + this._documents = docs.concat() + this.selectionCount = this._documents.length + this.documentPreview = this._documents.slice(0, 10) + } + + submit() { + if (this.createdBundle) return + this.payload = { + document_ids: this._documents.map((doc) => doc.id), + file_version: this.form.value.shareArchiveVersion + ? FileVersion.Archive + : FileVersion.Original, + expiration_days: this.form.value.expirationDays, + } + this.buttonsEnabled = false + super.confirm() + } + + getShareUrl(bundle: ShareLinkBundleSummary): string { + const apiURL = new URL(environment.apiBaseUrl) + return `${apiURL.origin}${apiURL.pathname.replace(/\/api\/$/, '/share/')}${ + bundle.slug + }` + } + + copy(bundle: ShareLinkBundleSummary): void { + const success = this.clipboard.copy(this.getShareUrl(bundle)) + if (success) { + this.copied = true + this.toastService.showInfo($localize`Share link copied to clipboard.`) + setTimeout(() => { + this.copied = false + }, 3000) + } + } + + openManage(): void { + if (this.onOpenManage) { + this.onOpenManage() + } else { + this.cancel() + } + } + + statusLabel(status: ShareLinkBundleSummary['status']): string { + return SHARE_LINK_BUNDLE_STATUS_LABELS[status] ?? status + } + + fileVersionLabel(version: FileVersion): string { + return SHARE_LINK_BUNDLE_FILE_VERSION_LABELS[version] ?? version + } +} diff --git a/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html b/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html new file mode 100644 index 000000000..2f2155412 --- /dev/null +++ b/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html @@ -0,0 +1,156 @@ + + + + + diff --git a/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.scss b/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.scss new file mode 100644 index 000000000..c8ffc4d5d --- /dev/null +++ b/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.scss @@ -0,0 +1,4 @@ +:host ::ng-deep .popover { + min-width: 300px; + max-width: 400px; + } diff --git a/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.spec.ts b/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.spec.ts new file mode 100644 index 000000000..113cd65a3 --- /dev/null +++ b/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.spec.ts @@ -0,0 +1,251 @@ +import { Clipboard } from '@angular/cdk/clipboard' +import { + ComponentFixture, + TestBed, + fakeAsync, + tick, +} from '@angular/core/testing' +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' +import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' +import { of, throwError } from 'rxjs' +import { FileVersion } from 'src/app/data/share-link' +import { + ShareLinkBundleStatus, + ShareLinkBundleSummary, +} from 'src/app/data/share-link-bundle' +import { ShareLinkBundleService } from 'src/app/services/rest/share-link-bundle.service' +import { ToastService } from 'src/app/services/toast.service' +import { environment } from 'src/environments/environment' +import { ShareLinkBundleManageDialogComponent } from './share-link-bundle-manage-dialog.component' + +class MockShareLinkBundleService { + listAllBundles = jest.fn() + delete = jest.fn() + rebuildBundle = jest.fn() +} + +class MockToastService { + showInfo = jest.fn() + showError = jest.fn() +} + +describe('ShareLinkBundleManageDialogComponent', () => { + let component: ShareLinkBundleManageDialogComponent + let fixture: ComponentFixture + let service: MockShareLinkBundleService + let toastService: MockToastService + let clipboard: Clipboard + let activeModal: NgbActiveModal + let originalApiBaseUrl: string + + beforeEach(() => { + service = new MockShareLinkBundleService() + toastService = new MockToastService() + originalApiBaseUrl = environment.apiBaseUrl + + service.listAllBundles.mockReturnValue(of([])) + service.delete.mockReturnValue(of(true)) + service.rebuildBundle.mockReturnValue(of(sampleBundle())) + + TestBed.configureTestingModule({ + imports: [ + ShareLinkBundleManageDialogComponent, + NgxBootstrapIconsModule.pick(allIcons), + ], + providers: [ + NgbActiveModal, + { provide: ShareLinkBundleService, useValue: service }, + { provide: ToastService, useValue: toastService }, + ], + }) + + fixture = TestBed.createComponent(ShareLinkBundleManageDialogComponent) + component = fixture.componentInstance + clipboard = TestBed.inject(Clipboard) + activeModal = TestBed.inject(NgbActiveModal) + }) + + afterEach(() => { + component.ngOnDestroy() + fixture.destroy() + environment.apiBaseUrl = originalApiBaseUrl + jest.clearAllMocks() + }) + + const sampleBundle = (overrides: Partial = {}) => + ({ + id: 1, + slug: 'bundle-slug', + created: new Date().toISOString(), + document_count: 1, + documents: [1], + status: ShareLinkBundleStatus.Pending, + file_version: FileVersion.Archive, + last_error: undefined, + ...overrides, + }) as ShareLinkBundleSummary + + it('loads bundles on init and polls periodically', fakeAsync(() => { + const bundles = [sampleBundle({ status: ShareLinkBundleStatus.Ready })] + service.listAllBundles.mockReset() + service.listAllBundles + .mockReturnValueOnce(of(bundles)) + .mockReturnValue(of(bundles)) + + fixture.detectChanges() + tick() + + expect(service.listAllBundles).toHaveBeenCalledTimes(1) + expect(component.bundles).toEqual(bundles) + expect(component.loading).toBe(false) + expect(component.error).toBeNull() + + tick(5000) + expect(service.listAllBundles).toHaveBeenCalledTimes(2) + })) + + it('handles errors when loading bundles', fakeAsync(() => { + service.listAllBundles.mockReset() + service.listAllBundles + .mockReturnValueOnce(throwError(() => new Error('load fail'))) + .mockReturnValue(of([])) + + fixture.detectChanges() + tick() + + expect(component.error).toContain('Failed to load share link bundles.') + expect(toastService.showError).toHaveBeenCalled() + expect(component.loading).toBe(false) + + tick(5000) + expect(service.listAllBundles).toHaveBeenCalledTimes(2) + })) + + it('copies bundle links when ready', fakeAsync(() => { + jest.spyOn(clipboard, 'copy').mockReturnValue(true) + fixture.detectChanges() + tick() + + const readyBundle = sampleBundle({ + slug: 'ready-slug', + status: ShareLinkBundleStatus.Ready, + }) + component.copy(readyBundle) + + expect(clipboard.copy).toHaveBeenCalledWith( + component.getShareUrl(readyBundle) + ) + expect(component.copiedSlug).toBe('ready-slug') + expect(toastService.showInfo).toHaveBeenCalled() + + tick(3000) + expect(component.copiedSlug).toBeNull() + })) + + it('ignores copy requests for non-ready bundles', fakeAsync(() => { + const copySpy = jest.spyOn(clipboard, 'copy') + fixture.detectChanges() + tick() + component.copy(sampleBundle({ status: ShareLinkBundleStatus.Pending })) + expect(copySpy).not.toHaveBeenCalled() + })) + + it('deletes bundles and refreshes list', fakeAsync(() => { + service.listAllBundles.mockReturnValue(of([])) + service.delete.mockReturnValue(of(true)) + + fixture.detectChanges() + tick() + + component.delete(sampleBundle()) + tick() + + expect(service.delete).toHaveBeenCalled() + expect(toastService.showInfo).toHaveBeenCalledWith( + expect.stringContaining('deleted.') + ) + expect(service.listAllBundles).toHaveBeenCalledTimes(2) + expect(component.loading).toBe(false) + })) + + it('handles delete errors gracefully', fakeAsync(() => { + service.listAllBundles.mockReturnValue(of([])) + service.delete.mockReturnValue(throwError(() => new Error('delete fail'))) + + fixture.detectChanges() + tick() + + component.delete(sampleBundle()) + tick() + + expect(toastService.showError).toHaveBeenCalled() + expect(component.loading).toBe(false) + })) + + it('retries bundle build and replaces existing entry', fakeAsync(() => { + service.listAllBundles.mockReturnValue(of([])) + const updated = sampleBundle({ status: ShareLinkBundleStatus.Ready }) + service.rebuildBundle.mockReturnValue(of(updated)) + + fixture.detectChanges() + tick() + + component.bundles = [sampleBundle()] + component.retry(component.bundles[0]) + tick() + + expect(service.rebuildBundle).toHaveBeenCalledWith(updated.id) + expect(component.bundles[0].status).toBe(ShareLinkBundleStatus.Ready) + expect(toastService.showInfo).toHaveBeenCalled() + })) + + it('adds new bundle when retry returns unknown entry', fakeAsync(() => { + service.listAllBundles.mockReturnValue(of([])) + service.rebuildBundle.mockReturnValue( + of(sampleBundle({ id: 99, slug: 'new-slug' })) + ) + + fixture.detectChanges() + tick() + + component.bundles = [sampleBundle()] + component.retry({ id: 99 } as ShareLinkBundleSummary) + tick() + + expect(component.bundles.find((bundle) => bundle.id === 99)).toBeTruthy() + })) + + it('handles retry errors', fakeAsync(() => { + service.listAllBundles.mockReturnValue(of([])) + service.rebuildBundle.mockReturnValue(throwError(() => new Error('fail'))) + + fixture.detectChanges() + tick() + + component.retry(sampleBundle()) + tick() + + expect(toastService.showError).toHaveBeenCalled() + })) + + it('maps helpers and closes dialog', fakeAsync(() => { + service.listAllBundles.mockReturnValue(of([])) + fixture.detectChanges() + tick() + + expect(component.statusLabel(ShareLinkBundleStatus.Processing)).toContain( + 'Processing' + ) + expect(component.fileVersionLabel(FileVersion.Original)).toContain( + 'Original' + ) + + environment.apiBaseUrl = 'https://example.com/api/' + const url = component.getShareUrl(sampleBundle({ slug: 'sluggy' })) + expect(url).toBe('https://example.com/share/sluggy') + + const closeSpy = jest.spyOn(activeModal, 'close') + component.close() + expect(closeSpy).toHaveBeenCalled() + })) +}) diff --git a/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.ts b/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.ts new file mode 100644 index 000000000..6eef144f9 --- /dev/null +++ b/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.ts @@ -0,0 +1,177 @@ +import { Clipboard } from '@angular/cdk/clipboard' +import { CommonModule } from '@angular/common' +import { Component, OnDestroy, OnInit, inject } from '@angular/core' +import { NgbActiveModal, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap' +import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' +import { Subject, catchError, of, switchMap, takeUntil, timer } from 'rxjs' +import { FileVersion } from 'src/app/data/share-link' +import { + SHARE_LINK_BUNDLE_FILE_VERSION_LABELS, + SHARE_LINK_BUNDLE_STATUS_LABELS, + ShareLinkBundleStatus, + ShareLinkBundleSummary, +} from 'src/app/data/share-link-bundle' +import { FileSizePipe } from 'src/app/pipes/file-size.pipe' +import { ShareLinkBundleService } from 'src/app/services/rest/share-link-bundle.service' +import { ToastService } from 'src/app/services/toast.service' +import { environment } from 'src/environments/environment' +import { LoadingComponentWithPermissions } from '../../loading-component/loading.component' +import { ConfirmButtonComponent } from '../confirm-button/confirm-button.component' + +@Component({ + selector: 'pngx-share-link-bundle-manage-dialog', + templateUrl: './share-link-bundle-manage-dialog.component.html', + styleUrls: ['./share-link-bundle-manage-dialog.component.scss'], + imports: [ + ConfirmButtonComponent, + CommonModule, + NgbPopoverModule, + NgxBootstrapIconsModule, + FileSizePipe, + ], +}) +export class ShareLinkBundleManageDialogComponent + extends LoadingComponentWithPermissions + implements OnInit, OnDestroy +{ + private readonly activeModal = inject(NgbActiveModal) + private readonly shareLinkBundleService = inject(ShareLinkBundleService) + private readonly toastService = inject(ToastService) + private readonly clipboard = inject(Clipboard) + + title = $localize`Share link bundles` + + bundles: ShareLinkBundleSummary[] = [] + error: string | null = null + copiedSlug: string | null = null + + readonly statuses = ShareLinkBundleStatus + readonly fileVersions = FileVersion + + private readonly refresh$ = new Subject() + + ngOnInit(): void { + this.refresh$ + .pipe( + switchMap((silent) => { + if (!silent) { + this.loading = true + } + this.error = null + return this.shareLinkBundleService.listAllBundles().pipe( + catchError((error) => { + if (!silent) { + this.loading = false + } + this.error = $localize`Failed to load share link bundles.` + this.toastService.showError( + $localize`Error retrieving share link bundles.`, + error + ) + return of(null) + }) + ) + }), + takeUntil(this.unsubscribeNotifier) + ) + .subscribe((results) => { + if (results) { + this.bundles = results + this.copiedSlug = null + } + this.loading = false + }) + + this.triggerRefresh(false) + timer(5000, 5000) + .pipe(takeUntil(this.unsubscribeNotifier)) + .subscribe(() => this.triggerRefresh(true)) + } + + ngOnDestroy(): void { + super.ngOnDestroy() + } + + getShareUrl(bundle: ShareLinkBundleSummary): string { + const apiURL = new URL(environment.apiBaseUrl) + return `${apiURL.origin}${apiURL.pathname.replace(/\/api\/$/, '/share/')}${ + bundle.slug + }` + } + + copy(bundle: ShareLinkBundleSummary): void { + if (bundle.status !== ShareLinkBundleStatus.Ready) { + return + } + const success = this.clipboard.copy(this.getShareUrl(bundle)) + if (success) { + this.copiedSlug = bundle.slug + setTimeout(() => { + this.copiedSlug = null + }, 3000) + this.toastService.showInfo($localize`Share link copied to clipboard.`) + } + } + + delete(bundle: ShareLinkBundleSummary): void { + this.error = null + this.loading = true + this.shareLinkBundleService.delete(bundle).subscribe({ + next: () => { + this.toastService.showInfo($localize`Share link bundle deleted.`) + this.triggerRefresh(false) + }, + error: (e) => { + this.loading = false + this.toastService.showError( + $localize`Error deleting share link bundle.`, + e + ) + }, + }) + } + + retry(bundle: ShareLinkBundleSummary): void { + this.error = null + this.shareLinkBundleService.rebuildBundle(bundle.id).subscribe({ + next: (updated) => { + this.toastService.showInfo( + $localize`Share link bundle rebuild requested.` + ) + this.replaceBundle(updated) + }, + error: (e) => { + this.toastService.showError($localize`Error requesting rebuild.`, e) + }, + }) + } + + statusLabel(status: ShareLinkBundleStatus): string { + return SHARE_LINK_BUNDLE_STATUS_LABELS[status] ?? status + } + + fileVersionLabel(version: FileVersion): string { + return SHARE_LINK_BUNDLE_FILE_VERSION_LABELS[version] ?? version + } + + close(): void { + this.activeModal.close() + } + + private replaceBundle(updated: ShareLinkBundleSummary): void { + const index = this.bundles.findIndex((bundle) => bundle.id === updated.id) + if (index >= 0) { + this.bundles = [ + ...this.bundles.slice(0, index), + updated, + ...this.bundles.slice(index + 1), + ] + } else { + this.bundles = [updated, ...this.bundles] + } + } + + private triggerRefresh(silent: boolean): void { + this.refresh$.next(silent) + } +} diff --git a/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.html b/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.html index fe3f9b9c3..e41a897a8 100644 --- a/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.html +++ b/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.html @@ -51,7 +51,7 @@
    diff --git a/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.ts b/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.ts index ffe11808c..9df3d438b 100644 --- a/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.ts +++ b/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.ts @@ -4,7 +4,11 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { first } from 'rxjs' -import { FileVersion, ShareLink } from 'src/app/data/share-link' +import { + FileVersion, + SHARE_LINK_EXPIRATION_OPTIONS, + ShareLink, +} from 'src/app/data/share-link' import { ShareLinkService } from 'src/app/services/rest/share-link.service' import { ToastService } from 'src/app/services/toast.service' import { environment } from 'src/environments/environment' @@ -21,12 +25,7 @@ export class ShareLinksDialogComponent implements OnInit { private toastService = inject(ToastService) private clipboard = inject(Clipboard) - EXPIRATION_OPTIONS = [ - { label: $localize`1 day`, value: 1 }, - { label: $localize`7 days`, value: 7 }, - { label: $localize`30 days`, value: 30 }, - { label: $localize`Never`, value: null }, - ] + readonly expirationOptions = SHARE_LINK_EXPIRATION_OPTIONS @Input() title = $localize`Share Links` diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html index 5ca002479..306152cc4 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.html +++ b/src-ui/src/app/components/document-detail/document-detail.component.html @@ -146,16 +146,26 @@
    - + @if (!isFieldHidden(DocumentDetailFieldID.ArchiveSerialNumber)) { + + } - - - - + @if (!isFieldHidden(DocumentDetailFieldID.Correspondent)) { + + } + @if (!isFieldHidden(DocumentDetailFieldID.DocumentType)) { + + } + @if (!isFieldHidden(DocumentDetailFieldID.StoragePath)) { + + } + @if (!isFieldHidden(DocumentDetailFieldID.Tags)) { + + } @for (fieldInstance of document?.custom_fields; track fieldInstance.field; let i = $index) {
    @switch (getCustomFieldFromInstance(fieldInstance)?.data_type) { diff --git a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts index d1d10c985..809478816 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts @@ -48,6 +48,7 @@ import { } from 'src/app/data/filter-rule-type' import { StoragePath } from 'src/app/data/storage-path' import { Tag } from 'src/app/data/tag' +import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { PermissionsGuard } from 'src/app/guards/permissions.guard' import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe' @@ -1015,7 +1016,7 @@ describe('DocumentDetailComponent', () => { it('should display built-in pdf viewer if not disabled', () => { initNormally() component.document.archived_file_name = 'file.pdf' - jest.spyOn(settingsService, 'get').mockReturnValue(false) + settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, false) expect(component.useNativePdfViewer).toBeFalsy() fixture.detectChanges() expect(fixture.debugElement.query(By.css('pdf-viewer'))).not.toBeNull() @@ -1024,7 +1025,7 @@ describe('DocumentDetailComponent', () => { it('should display native pdf viewer if enabled', () => { initNormally() component.document.archived_file_name = 'file.pdf' - jest.spyOn(settingsService, 'get').mockReturnValue(true) + settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, true) expect(component.useNativePdfViewer).toBeTruthy() fixture.detectChanges() expect(fixture.debugElement.query(By.css('object'))).not.toBeNull() diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index 7bf61ddde..f48001b97 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -84,6 +84,7 @@ import { ToastService } from 'src/app/services/toast.service' import { getFilenameFromContentDisposition } from 'src/app/utils/http' import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter' import * as UTIF from 'utif' +import { DocumentDetailFieldID } from '../admin/settings/settings.component' import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component' import { PasswordRemovalConfirmDialogComponent } from '../common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component' import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component' @@ -282,6 +283,8 @@ export class DocumentDetailComponent public readonly DataType = DataType + public readonly DocumentDetailFieldID = DocumentDetailFieldID + @ViewChild('nav') nav: NgbNav @ViewChild('pdfPreview') set pdfPreview(element) { // this gets called when component added or removed from DOM @@ -328,6 +331,12 @@ export class DocumentDetailComponent return this.settings.get(SETTINGS_KEYS.DOCUMENT_EDITING_OVERLAY_THUMBNAIL) } + isFieldHidden(fieldId: DocumentDetailFieldID): boolean { + return this.settings + .get(SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS) + .includes(fieldId) + } + private getRenderType(mimeType: string): ContentRenderType { if (!mimeType) return ContentRenderType.Unknown if (mimeType === 'application/pdf') { diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html index 2323929d1..6f3a84eee 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html @@ -96,14 +96,36 @@ - @if (emailEnabled) { - - }
    +
    + +
    + + + + @if (emailEnabled) { + + } +
    +