Squashed commit of the following:

commit 8a0a49dd5766094f60462fbfbe62e9921fbd2373
Author: shamoon <4887959+shamoon@users.noreply.github.com>
Date:   Tue Dec 19 23:02:47 2023 -0800

    Fix formatting

commit 66b2d90c507b8afd9507813ff555e46198ea33b9
Author: shamoon <4887959+shamoon@users.noreply.github.com>
Date:   Tue Dec 19 22:36:35 2023 -0800

    Refactor frontend data models

commit 5723bd8dd823ee855625e250df39393e26709d48
Author: Adam Bogdał <adam@bogdal.pl>
Date:   Wed Dec 20 01:17:43 2023 +0100

    Fix: speed up admin panel for installs with a large number of documents (#5052)

commit 9b08ce176199bf9011a6634bb88f616846150d2b
Author: shamoon <4887959+shamoon@users.noreply.github.com>
Date:   Tue Dec 19 15:18:51 2023 -0800

    Update PULL_REQUEST_TEMPLATE.md

commit a6248bec2d793b7690feed95fcaf5eb34a75bfb6
Author: shamoon <4887959+shamoon@users.noreply.github.com>
Date:   Tue Dec 19 15:02:05 2023 -0800

    Chore: Update Angular to v17 (#4980)

commit b1f6f52486d5ba5c04af99b41315eb6428fd1fa8
Author: shamoon <4887959+shamoon@users.noreply.github.com>
Date:   Tue Dec 19 13:53:56 2023 -0800

    Fix: Dont allow null custom_fields property via API (#5063)

commit 638d9970fd468d8c02c91d19bd28f8b0796bdcb1
Author: shamoon <4887959+shamoon@users.noreply.github.com>
Date:   Tue Dec 19 13:43:50 2023 -0800

    Enhancement: symmetric document links (#4907)

commit 5e8de4c1da6eb4eb8f738b20962595c7536b30ec
Author: shamoon <4887959+shamoon@users.noreply.github.com>
Date:   Tue Dec 19 12:45:04 2023 -0800

    Enhancement: shared icon & shared by me filter (#4859)

commit 088bad90306025d3f6b139cbd0ad264a1cbecfe5
Author: Trenton H <797416+stumpylog@users.noreply.github.com>
Date:   Tue Dec 19 12:04:03 2023 -0800

    Bulk updates all the backend libraries (#5061)
This commit is contained in:
shamoon 2023-12-19 23:03:10 -08:00
parent 74e845974c
commit 3e1a3aef4c
209 changed files with 9125 additions and 7680 deletions

View File

@ -8,7 +8,11 @@ Note: All PRs with code changes should be targeted to the `dev` branch, pure doc
Please include a summary of the change and which issue is fixed (if any) and any relevant motivation / context. List any dependencies that are required for this change. If appropriate, please include an explanation of how your proposed change can be tested. Screenshots and / or videos can also be helpful if appropriate.
-->
Fixes # (issue)
<!--
⚠️ Important: Pull requests that implement a new feature *should almost always target an existing feature request*. This is in order to balance the work of implementing and maintaining new features vs. community-interest. If that is not currently the case, please open a feature request instead of this PR to gather feedback from both users and the project maintainers.
-->
Closes #(issue or discussion)
## Type of change
@ -17,10 +21,11 @@ What type of change does your PR introduce to Paperless-ngx?
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):
- [ ] Bug fix: non-breaking change which fixes an issue.
- [ ] New feature: non-breaking change which adds functionality. _Please read the important note above._
- [ ] Breaking change: fix or feature that would cause existing functionality to not work as expected.
- [ ] Documentation only.
- [ ] Other. Please explain:
## Checklist:

View File

@ -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.10.24"
DEFAULT_PIP_ENV_VERSION: "2023.11.15"
# This is the default version of Python to use in most steps which aren't specific
DEFAULT_PYTHON_VERSION: "3.10"

View File

@ -94,7 +94,7 @@ The following files need to be changed:
- src-ui/angular.json (under the _projects/paperless-ui/i18n/locales_ JSON key)
- src/paperless/settings.py (in the _LANGUAGES_ array)
- src-ui/src/app/services/settings.service.ts (inside the _getLanguageOptions_ method)
- src-ui/src/app/services/settings.service.ts (inside the _LANGUAGE_OPTIONS_ array)
- src-ui/src/app/app.module.ts (import locale from _angular/common/locales_ and call _registerLocaleData_)
Please add the language in the correct order, alphabetically by locale.

View File

@ -12,7 +12,7 @@ COPY ./src-ui /src/src-ui
WORKDIR /src/src-ui
RUN set -eux \
&& npm update npm -g \
&& npm ci --omit=optional
&& npm ci
RUN set -eux \
&& ./node_modules/.bin/ng build --configuration production
@ -29,7 +29,7 @@ COPY Pipfile* ./
RUN set -eux \
&& echo "Installing pipenv" \
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2023.10.24 \
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2023.11.15 \
&& echo "Generating requirement.txt" \
&& pipenv requirements > requirements.txt

View File

@ -57,7 +57,7 @@ zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"}
[dev-packages]
# Linting
black = "*"
black = "==23.11.0"
pre-commit = "*"
ruff = "*"
# Testing

837
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -80,7 +80,7 @@ django_checks() {
search_index() {
local -r index_version=7
local -r index_version=8
local -r index_version_file=${DATA_DIR}/.index_version
if [[ (! -f "${index_version_file}") || $(<"${index_version_file}") != "$index_version" ]]; then

View File

@ -277,27 +277,17 @@ Adding new languages requires adding the translated files in the
}
```
2. Add the language to the available options in
2. Add the language to the `LANGUAGE_OPTIONS` array in
`src-ui/src/app/services/settings.service.ts`:
```typescript
getLanguageOptions(): LanguageOption[] {
return [
{code: "en-us", name: $localize`English (US)`, englishName: "English (US)", dateInputFormat: "mm/dd/yyyy"},
{code: "en-gb", name: $localize`English (GB)`, englishName: "English (GB)", dateInputFormat: "dd/mm/yyyy"},
{code: "de", name: $localize`German`, englishName: "German", dateInputFormat: "dd.mm.yyyy"},
{code: "nl", name: $localize`Dutch`, englishName: "Dutch", dateInputFormat: "dd-mm-yyyy"},
{code: "fr", name: $localize`French`, englishName: "French", dateInputFormat: "dd/mm/yyyy"},
{code: "pt-br", name: $localize`Portuguese (Brazil)`, englishName: "Portuguese (Brazil)", dateInputFormat: "dd/mm/yyyy"}
// Add your new language here
]
}
```
`dateInputFormat` is a special string that defines the behavior of
the date input fields and absolutely needs to contain "dd", "mm"
and "yyyy".
```
3. Import and register the Angular data for this locale in
`src-ui/src/app/app.module.ts`:

View File

@ -345,7 +345,7 @@ The following custom field types are supported:
- `Integer`: integer number e.g. 12
- `Number`: float number e.g. 12.3456
- `Monetary`: float number with exactly two decimals, e.g. 12.30
- `Document Link`: reference(s) to other document(s), displayed as links
- `Document Link`: reference(s) to other document(s) displayed as links, automatically creates a symmetrical link in reverse
## Share Links

View File

@ -125,18 +125,18 @@
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "paperless-ui:build:en-US"
"buildTarget": "paperless-ui:build:en-US"
},
"configurations": {
"production": {
"browserTarget": "paperless-ui:build:production"
"buildTarget": "paperless-ui:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "paperless-ui:build"
"buildTarget": "paperless-ui:build"
}
},
"test": {

View File

@ -1657,7 +1657,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.html</context>
<context context-type="linenumber">68</context>
<context context-type="linenumber">78</context>
</context-group>
</trans-unit>
<trans-unit id="2941198503117307737" datatype="html">
@ -1721,7 +1721,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
<context context-type="linenumber">83</context>
<context context-type="linenumber">89</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/consumption-templates/consumption-templates.component.html</context>
@ -3670,18 +3670,25 @@
<context context-type="linenumber">38</context>
</context-group>
</trans-unit>
<trans-unit id="175385209536581523" datatype="html">
<source>Shared by me</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.html</context>
<context context-type="linenumber">48</context>
</context-group>
</trans-unit>
<trans-unit id="5151074932731293042" datatype="html">
<source>Unowned</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.html</context>
<context context-type="linenumber">48</context>
<context context-type="linenumber">58</context>
</context-group>
</trans-unit>
<trans-unit id="8999708063434507268" datatype="html">
<source>Hide unowned</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.html</context>
<context context-type="linenumber">77</context>
<context context-type="linenumber">87</context>
</context-group>
</trans-unit>
<trans-unit id="8650499415827640724" datatype="html">
@ -4012,7 +4019,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">203</context>
<context context-type="linenumber">204</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/rest/document.service.ts</context>
@ -4073,7 +4080,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
<context context-type="linenumber">99</context>
<context context-type="linenumber">105</context>
</context-group>
</trans-unit>
<trans-unit id="872092479747931526" datatype="html">
@ -5007,11 +5014,26 @@
<context context-type="linenumber">58,59</context>
</context-group>
</trans-unit>
<trans-unit id="5739581984228459958" datatype="html">
<source>Shared</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
<context context-type="linenumber">119</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
<context context-type="linenumber">84</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/pipes/username.pipe.ts</context>
<context context-type="linenumber">33</context>
</context-group>
</trans-unit>
<trans-unit id="2332107018974972998" datatype="html">
<source>Score:</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
<context context-type="linenumber">116</context>
<context context-type="linenumber">122</context>
</context-group>
</trans-unit>
<trans-unit id="3661756380991326939" datatype="html">
@ -5138,7 +5160,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">208</context>
<context context-type="linenumber">209</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/rest/document.service.ts</context>
@ -5254,14 +5276,14 @@
)?.name"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">120,122</context>
<context context-type="linenumber">121,123</context>
</context-group>
</trans-unit>
<trans-unit id="8170755470576301659" datatype="html">
<source>Without correspondent</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">124</context>
<context context-type="linenumber">125</context>
</context-group>
</trans-unit>
<trans-unit id="317796810569008208" datatype="html">
@ -5270,14 +5292,14 @@
)?.name"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">130,132</context>
<context context-type="linenumber">131,133</context>
</context-group>
</trans-unit>
<trans-unit id="4362173610367509215" datatype="html">
<source>Without document type</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">134</context>
<context context-type="linenumber">135</context>
</context-group>
</trans-unit>
<trans-unit id="232202047340644471" datatype="html">
@ -5286,14 +5308,14 @@
)?.name"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">140,142</context>
<context context-type="linenumber">141,143</context>
</context-group>
</trans-unit>
<trans-unit id="1562820715074533164" datatype="html">
<source>Without storage path</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">144</context>
<context context-type="linenumber">145</context>
</context-group>
</trans-unit>
<trans-unit id="8180755793012580465" datatype="html">
@ -5301,112 +5323,112 @@
?.name"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">148,149</context>
<context context-type="linenumber">149,150</context>
</context-group>
</trans-unit>
<trans-unit id="6494566478302448576" datatype="html">
<source>Without any tag</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">153</context>
<context context-type="linenumber">154</context>
</context-group>
</trans-unit>
<trans-unit id="6523384805359286307" datatype="html">
<source>Title: <x id="PH" equiv-text="rule.value"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">157</context>
<context context-type="linenumber">158</context>
</context-group>
</trans-unit>
<trans-unit id="1872523635812236432" datatype="html">
<source>ASN: <x id="PH" equiv-text="rule.value"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">160</context>
<context context-type="linenumber">161</context>
</context-group>
</trans-unit>
<trans-unit id="102674688969746976" datatype="html">
<source>Owner: <x id="PH" equiv-text="rule.value"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">163</context>
<context context-type="linenumber">164</context>
</context-group>
</trans-unit>
<trans-unit id="3550877650686009106" datatype="html">
<source>Owner not in: <x id="PH" equiv-text="rule.value"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">166</context>
<context context-type="linenumber">167</context>
</context-group>
</trans-unit>
<trans-unit id="1082034558646673343" datatype="html">
<source>Without an owner</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">169</context>
<context context-type="linenumber">170</context>
</context-group>
</trans-unit>
<trans-unit id="3100631071441658964" datatype="html">
<source>Title &amp; content</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">206</context>
<context context-type="linenumber">207</context>
</context-group>
</trans-unit>
<trans-unit id="9149498548977462220" datatype="html">
<source>Custom fields</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">211</context>
<context context-type="linenumber">212</context>
</context-group>
</trans-unit>
<trans-unit id="1010505078885609376" datatype="html">
<source>Advanced search</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">215</context>
<context context-type="linenumber">216</context>
</context-group>
</trans-unit>
<trans-unit id="2649431021108393503" datatype="html">
<source>More like</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">221</context>
<context context-type="linenumber">222</context>
</context-group>
</trans-unit>
<trans-unit id="3697582909018473071" datatype="html">
<source>equals</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">240</context>
<context context-type="linenumber">241</context>
</context-group>
</trans-unit>
<trans-unit id="5325481293405718739" datatype="html">
<source>is empty</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">244</context>
<context context-type="linenumber">245</context>
</context-group>
</trans-unit>
<trans-unit id="6166785695326182482" datatype="html">
<source>is not empty</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">248</context>
<context context-type="linenumber">249</context>
</context-group>
</trans-unit>
<trans-unit id="4686622206659266699" datatype="html">
<source>greater than</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">252</context>
<context context-type="linenumber">253</context>
</context-group>
</trans-unit>
<trans-unit id="8014012170270529279" datatype="html">
<source>less than</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">256</context>
<context context-type="linenumber">257</context>
</context-group>
</trans-unit>
<trans-unit id="7210076240260527720" datatype="html">
@ -6297,13 +6319,6 @@
<context context-type="linenumber">11</context>
</context-group>
</trans-unit>
<trans-unit id="5739581984228459958" datatype="html">
<source>Shared</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/pipes/username.pipe.ts</context>
<context context-type="linenumber">33</context>
</context-group>
</trans-unit>
<trans-unit id="2807800733729323332" datatype="html">
<source>Yes</source>
<context-group purpose="location">

7697
src-ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,56 +11,56 @@
},
"private": true,
"dependencies": {
"@angular/cdk": "^16.2.11",
"@angular/common": "~16.2.11",
"@angular/compiler": "~16.2.11",
"@angular/core": "~16.2.11",
"@angular/forms": "~16.2.11",
"@angular/localize": "~16.2.11",
"@angular/platform-browser": "~16.2.11",
"@angular/platform-browser-dynamic": "~16.2.11",
"@angular/router": "~16.2.11",
"@ng-bootstrap/ng-bootstrap": "^15.1.2",
"@ng-select/ng-select": "^11.2.0",
"@angular/cdk": "^17.0.4",
"@angular/common": "~17.0.7",
"@angular/compiler": "~17.0.7",
"@angular/core": "~17.0.7",
"@angular/forms": "~17.0.7",
"@angular/localize": "~17.0.7",
"@angular/platform-browser": "~17.0.7",
"@angular/platform-browser-dynamic": "~17.0.7",
"@angular/router": "~17.0.7",
"@ng-bootstrap/ng-bootstrap": "^16.0.0",
"@ng-select/ng-select": "^12.0.4",
"@ngneat/dirty-check-forms": "^3.0.3",
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.2",
"file-saver": "^2.0.5",
"mime-names": "^1.0.0",
"ngx-color": "^9.0.0",
"ngx-cookie-service": "^16.0.1",
"ngx-cookie-service": "^17.0.1",
"ngx-file-drop": "^16.0.0",
"ngx-ui-tour-ng-bootstrap": "^13.0.6",
"ngx-ui-tour-ng-bootstrap": "^14.0.1",
"pdfjs-dist": "^3.11.174",
"rxjs": "^7.8.1",
"tslib": "^2.6.2",
"uuid": "^9.0.1",
"zone.js": "^0.13.3"
"zone.js": "^0.14.2"
},
"devDependencies": {
"@angular-builders/jest": "16.0.1",
"@angular-devkit/build-angular": "~16.2.9",
"@angular-eslint/builder": "16.2.0",
"@angular-eslint/eslint-plugin": "16.2.0",
"@angular-eslint/eslint-plugin-template": "16.2.0",
"@angular-eslint/schematics": "16.2.0",
"@angular-eslint/template-parser": "16.2.0",
"@angular/cli": "~16.2.9",
"@angular/compiler-cli": "~16.2.3",
"@angular-builders/jest": "17.0.0",
"@angular-devkit/build-angular": "~17.0.7",
"@angular-eslint/builder": "17.1.1",
"@angular-eslint/eslint-plugin": "17.1.1",
"@angular-eslint/eslint-plugin-template": "17.1.1",
"@angular-eslint/schematics": "17.1.1",
"@angular-eslint/template-parser": "17.1.1",
"@angular/cli": "~17.0.7",
"@angular/compiler-cli": "~17.0.7",
"@playwright/test": "^1.40.1",
"@types/jest": "^29.5.10",
"@types/node": "^20.10.2",
"@typescript-eslint/eslint-plugin": "^6.13.1",
"@typescript-eslint/parser": "^6.13.1",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"concurrently": "^8.2.2",
"eslint": "^8.55.0",
"eslint": "^8.53.0",
"jest": "29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-preset-angular": "^13.1.4",
"jest-websocket-mock": "^2.5.0",
"patch-package": "^8.0.0",
"ts-node": "~10.9.1",
"typescript": "^5.1.6",
"typescript": "^5.2.2",
"wait-on": "^7.2.0"
}
}

View File

@ -19,12 +19,16 @@
</button>
</div>
<div class="btn-group btn-group-sm align-self-end" role="group" aria-label="Previous / Next">
<button *ngIf="tourService.hasPrev(step)" class="btn btn-outline-primary" (click)="tourService.prev()">
@if (tourService.hasPrev(step)) {
<button class="btn btn-outline-primary" (click)="tourService.prev()">
« {{ step?.prevBtnTitle }}
</button>
<button *ngIf="tourService.hasNext(step)" class="btn btn-outline-primary" (click)="tourService.next()">
}
@if (tourService.hasNext(step)) {
<button class="btn btn-outline-primary" (click)="tourService.next()">
{{ step?.nextBtnTitle }} »
</button>
}
</div>
</div>
</div>

View File

@ -1,5 +1,5 @@
import { SettingsService } from './services/settings.service'
import { SETTINGS_KEYS } from './data/paperless-uisettings'
import { SETTINGS_KEYS } from './data/ui-settings'
import { Component, OnDestroy, OnInit, Renderer2 } from '@angular/core'
import { Router } from '@angular/router'
import { Subscription, first } from 'rxjs'

View File

@ -6,25 +6,35 @@
</pngx-page-header>
<ul ngbNav #nav="ngbNav" [(activeId)]="activeLog" (activeIdChange)="reloadLogs()" class="nav-tabs">
<li *ngFor="let logFile of logFiles" [ngbNavItem]="logFile">
@for (logFile of logFiles; track logFile) {
<li [ngbNavItem]="logFile">
<a ngbNavLink>
{{logFile}}.log
</a>
</li>
<div *ngIf="isLoading || !logFiles.length" class="ps-2 d-flex align-items-center">
}
@if (isLoading || !logFiles.length) {
<div class="ps-2 d-flex align-items-center">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container *ngIf="!logFiles.length" i18n>Loading...</ng-container>
@if (!logFiles.length) {
<ng-container i18n>Loading...</ng-container>
}
</div>
}
</ul>
<div [ngbNavOutlet]="nav" class="mt-2"></div>
<div class="bg-dark p-3 text-light font-monospace log-container" #logContainer>
<div *ngIf="isLoading && logFiles.length">
@if (isLoading && logFiles.length) {
<div>
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container>
</div>
}
@for (log of logs; track log) {
<p
class="m-0 p-0 log-entry-{{getLogLevel(log)}}"
*ngFor="let log of logs">{{log}}</p>
>{{log}}</p>
}
</div>

View File

@ -24,10 +24,16 @@
<div class="col">
<select class="form-select" formControlName="displayLanguage">
<option *ngFor="let lang of displayLanguageOptions" [ngValue]="lang.code">{{lang.name}}<span *ngIf="lang.code && currentLocale !== 'en-US'"> - {{lang.englishName}}</span></option>
@for (lang of displayLanguageOptions; track lang) {
<option [ngValue]="lang.code">{{lang.name}}@if (lang.code && currentLocale !== 'en-US') {
<span> - {{lang.englishName}}</span>
}</option>
}
</select>
<small *ngIf="displayLanguageIsDirty" class="form-text text-primary" i18n>You need to reload the page after applying a new language.</small>
@if (displayLanguageIsDirty) {
<small class="form-text text-primary" i18n>You need to reload the page after applying a new language.</small>
}
</div>
</div>
@ -39,7 +45,11 @@
<div class="col">
<select class="form-select" formControlName="dateLocale">
<option *ngFor="let lang of dateLocaleOptions" [ngValue]="lang.code">{{lang.name}}<span *ngIf="lang.code"> - {{today | customDate:'shortDate':null:lang.code}}</span></option>
@for (lang of dateLocaleOptions; track lang) {
<option [ngValue]="lang.code">{{lang.name}}@if (lang.code) {
<span> - {{today | customDate:'shortDate':null:lang.code}}</span>
}</option>
}
</select>
</div>
@ -281,12 +291,12 @@
<h4 i18n>Views</h4>
<div formGroupName="savedViews">
<div *ngFor="let view of savedViews" [formGroupName]="view.id" class="row">
@for (view of savedViews; track view) {
<div [formGroupName]="view.id" class="row">
<div class="mb-3 col">
<label class="form-label" for="name_{{view.id}}" i18n>Name</label>
<input type="text" class="form-control" formControlName="name" id="name_{{view.id}}">
</div>
<div class="mb-2 col">
<label class="form-label" for="show_on_dashboard_{{view.id}}" i18n>&nbsp;<span class="visually-hidden">Appears on</span></label>
<div class="form-check form-switch">
@ -298,19 +308,23 @@
<label class="form-check-label" for="show_in_sidebar_{{view.id}}" i18n>Show in sidebar</label>
</div>
</div>
<div class="mb-2 col-auto">
<label class="form-label" for="name_{{view.id}}" i18n>Actions</label>
<button type="button" class="btn btn-sm btn-outline-danger form-control" (click)="deleteSavedView(view)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.SavedView }" i18n>Delete</button>
</div>
</div>
}
<div *ngIf="savedViews && savedViews.length === 0" i18n>No saved views defined.</div>
@if (savedViews && savedViews.length === 0) {
<div i18n>No saved views defined.</div>
}
<div *ngIf="!savedViews">
@if (!savedViews) {
<div>
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div>
</div>
}
</div>

View File

@ -13,8 +13,8 @@ import {
import { NgSelectModule } from '@ng-select/ng-select'
import { of, throwError } from 'rxjs'
import { routes } from 'src/app/app-routing.module'
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
import { SavedView } from 'src/app/data/saved-view'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
@ -138,7 +138,7 @@ describe('SettingsComponent', () => {
of({
all: savedViews.map((v) => v.id),
count: savedViews.length,
results: (savedViews as PaperlessSavedView[]).concat([]),
results: (savedViews as SavedView[]).concat([]),
})
)
}
@ -226,9 +226,7 @@ describe('SettingsComponent', () => {
savedViewPatchSpy.mockClear()
// succeed saved views
savedViewPatchSpy.mockReturnValueOnce(
of(savedViews as PaperlessSavedView[])
)
savedViewPatchSpy.mockReturnValueOnce(of(savedViews as SavedView[]))
component.saveSettings()
expect(toastErrorSpy).not.toHaveBeenCalled()
expect(savedViewPatchSpy).toHaveBeenCalled()
@ -335,7 +333,7 @@ describe('SettingsComponent', () => {
const toastSpy = jest.spyOn(toastService, 'showInfo')
const deleteSpy = jest.spyOn(savedViewService, 'delete')
deleteSpy.mockReturnValue(of(true))
component.deleteSavedView(savedViews[0] as PaperlessSavedView)
component.deleteSavedView(savedViews[0] as SavedView)
expect(deleteSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalledWith(
`Saved view "${savedViews[0].name}" deleted.`

View File

@ -21,10 +21,10 @@ import {
takeUntil,
tap,
} from 'rxjs'
import { PaperlessGroup } from 'src/app/data/paperless-group'
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
import { PaperlessUser } from 'src/app/data/paperless-user'
import { Group } from 'src/app/data/group'
import { SavedView } from 'src/app/data/saved-view'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { User } from 'src/app/data/user'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import {
PermissionsService,
@ -48,6 +48,12 @@ enum SettingsNavIDs {
SavedViews = 4,
}
const systemLanguage = { code: '', name: $localize`Use system language` }
const systemDateFormat = {
code: '',
name: $localize`Use date format of display language`,
}
@Component({
selector: 'pngx-settings',
templateUrl: './settings.component.html',
@ -92,7 +98,7 @@ export class SettingsComponent
savedViews: this.savedViewGroup,
})
savedViews: PaperlessSavedView[]
savedViews: SavedView[]
store: BehaviorSubject<any>
storeSub: Subscription
@ -101,8 +107,8 @@ export class SettingsComponent
unsubscribeNotifier: Subject<any> = new Subject()
savePending: boolean = false
users: PaperlessUser[]
groups: PaperlessGroup[]
users: User[]
groups: Group[]
get computedDateLocale(): string {
return (
@ -362,7 +368,7 @@ export class SettingsComponent
this.settings.organizingSidebarSavedViews = false
}
deleteSavedView(savedView: PaperlessSavedView) {
deleteSavedView(savedView: SavedView) {
this.savedViewService.delete(savedView).subscribe(() => {
this.savedViewGroup.removeControl(savedView.id.toString())
this.savedViews.splice(this.savedViews.indexOf(savedView), 1)
@ -512,15 +518,11 @@ export class SettingsComponent
}
get displayLanguageOptions(): LanguageOption[] {
return [{ code: '', name: $localize`Use system language` }].concat(
this.settings.getLanguageOptions()
)
return [systemLanguage].concat(this.settings.getLanguageOptions())
}
get dateLocaleOptions(): LanguageOption[] {
return [
{ code: '', name: $localize`Use date format of display language` },
].concat(this.settings.getDateLocaleOptions())
return [systemDateFormat].concat(this.settings.getDateLocaleOptions())
}
get today() {
@ -529,7 +531,7 @@ export class SettingsComponent
saveSettings() {
// only patch views that have actually changed
const changed: PaperlessSavedView[] = []
const changed: SavedView[] = []
Object.values(this.savedViewGroup.controls)
.filter((g: FormGroup) => !g.pristine)
.forEach((group: FormGroup) => {

View File

@ -17,10 +17,10 @@
</div>
</pngx-page-header>
<ng-container *ngIf="!tasksService.completedFileTasks && tasksService.loading">
@if (!tasksService.completedFileTasks && tasksService.loading) {
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div>
</ng-container>
}
<ng-template let-tasks="tasks" #tasksTemplate>
<table class="table table-striped align-middle border shadow-sm">
@ -34,13 +34,15 @@
</th>
<th scope="col" i18n>Name</th>
<th scope="col" class="d-none d-lg-table-cell" i18n>Created</th>
<th scope="col" class="d-none d-lg-table-cell" *ngIf="activeTab !== 'started' && activeTab !== 'queued'" i18n>Results</th>
@if (activeTab !== 'started' && activeTab !== 'queued') {
<th scope="col" class="d-none d-lg-table-cell" i18n>Results</th>
}
<th scope="col" class="d-table-cell d-lg-none" i18n>Info</th>
<th scope="col" i18n>Actions</th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let task of tasks | slice: (page-1) * pageSize : page * pageSize">
@for (task of tasks | slice: (page-1) * pageSize : page * pageSize; track task) {
<tr (click)="toggleSelected(task, $event); $event.stopPropagation();">
<td>
<div class="form-check">
@ -50,17 +52,27 @@
</td>
<td class="overflow-auto name-col">{{ task.task_file_name }}</td>
<td class="d-none d-lg-table-cell">{{ task.date_created | customDate:'short' }}</td>
<td class="d-none d-lg-table-cell" *ngIf="activeTab !== 'started' && activeTab !== 'queued'">
<div *ngIf="task.result?.length > 50" class="result" (click)="expandTask(task); $event.stopPropagation();"
@if (activeTab !== 'started' && activeTab !== 'queued') {
<td class="d-none d-lg-table-cell">
@if (task.result?.length > 50) {
<div class="result" (click)="expandTask(task); $event.stopPropagation();"
[ngbPopover]="resultPopover" popoverClass="shadow small mobile" triggers="mouseenter:mouseleave" container="body">
<span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result | slice:0:50 }}&hellip;</span>
</div>
<span *ngIf="task.result?.length <= 50" class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result }}</span>
}
@if (task.result?.length <= 50) {
<span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result }}</span>
}
<ng-template #resultPopover>
<pre class="small mb-0">{{ task.result | slice:0:300 }}<ng-container *ngIf="task.result.length > 300">&hellip;</ng-container></pre>
<ng-container *ngIf="task.result?.length > 300"><br/><em>(<ng-container i18n>click for full output</ng-container>)</em></ng-container>
<pre class="small mb-0">{{ task.result | slice:0:300 }}@if (task.result.length > 300) {
&hellip;
}</pre>
@if (task.result?.length > 300) {
<br/><em>(<ng-container i18n>click for full output</ng-container>)</em>
}
</ng-template>
</td>
}
<td class="d-lg-none">
<button class="btn btn-link" (click)="expandTask(task); $event.stopPropagation();">
<svg fill="currentColor" class="" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16">
@ -76,11 +88,13 @@
</svg>&nbsp;<ng-container i18n>Dismiss</ng-container>
</button>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
<button *ngIf="task.related_document" class="btn btn-sm btn-outline-primary" (click)="dismissAndGo(task); $event.stopPropagation();">
@if (task.related_document) {
<button class="btn btn-sm btn-outline-primary" (click)="dismissAndGo(task); $event.stopPropagation();">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
</svg>&nbsp;<ng-container i18n>Open Document</ng-container>
</button>
}
</ng-container>
</div>
</td>
@ -90,37 +104,49 @@
<pre #collapse="ngbCollapse" [ngbCollapse]="expandedTask !== task.id" class="small mb-0"><div class="small p-1 p-lg-3 ms-lg-3">{{ task.result }}</div></pre>
</td>
</tr>
</ng-container>
}
</tbody>
</table>
<div class="pb-3 d-sm-flex justify-content-between align-items-center">
<div class="pb-2 pb-sm-0" i18n *ngIf="tasks.length > 0">{tasks.length, plural, =1 {One {{this.activeTabLocalized}} task} other {{{tasks.length || 0}} total {{this.activeTabLocalized}} tasks}}</div>
<ngb-pagination *ngIf="tasks.length > pageSize" [(page)]="page" [pageSize]="pageSize" [collectionSize]="tasks.length" maxSize="8" size="sm"></ngb-pagination>
@if (tasks.length > 0) {
<div class="pb-2 pb-sm-0" i18n>{tasks.length, plural, =1 {One {{this.activeTabLocalized}} task} other {{{tasks.length || 0}} total {{this.activeTabLocalized}} tasks}}</div>
}
@if (tasks.length > pageSize) {
<ngb-pagination [(page)]="page" [pageSize]="pageSize" [collectionSize]="tasks.length" maxSize="8" size="sm"></ngb-pagination>
}
</div>
</ng-template>
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" (hidden)="duringTabChange($event)">
<li ngbNavItem="failed">
<a ngbNavLink i18n>Failed<span *ngIf="tasksService.failedFileTasks.length > 0" class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span></a>
<a ngbNavLink i18n>Failed@if (tasksService.failedFileTasks.length > 0) {
<span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.failedFileTasks}"></ng-container>
</ng-template>
</li>
<li ngbNavItem="completed">
<a ngbNavLink i18n>Complete<span *ngIf="tasksService.completedFileTasks.length > 0" class="badge bg-secondary ms-2">{{tasksService.completedFileTasks.length}}</span></a>
<a ngbNavLink i18n>Complete@if (tasksService.completedFileTasks.length > 0) {
<span class="badge bg-secondary ms-2">{{tasksService.completedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.completedFileTasks}"></ng-container>
</ng-template>
</li>
<li ngbNavItem="started">
<a ngbNavLink i18n>Started<span *ngIf="tasksService.startedFileTasks.length > 0" class="badge bg-secondary ms-2">{{tasksService.startedFileTasks.length}}</span></a>
<a ngbNavLink i18n>Started@if (tasksService.startedFileTasks.length > 0) {
<span class="badge bg-secondary ms-2">{{tasksService.startedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.startedFileTasks}"></ng-container>
</ng-template>
</li>
<li ngbNavItem="queued">
<a ngbNavLink i18n>Queued<span *ngIf="tasksService.queuedFileTasks.length > 0" class="badge bg-secondary ms-2">{{tasksService.queuedFileTasks.length}}</span></a>
<a ngbNavLink i18n>Queued@if (tasksService.queuedFileTasks.length > 0) {
<span class="badge bg-secondary ms-2">{{tasksService.queuedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.queuedFileTasks}"></ng-container>
</ng-template>

View File

@ -1,7 +1,7 @@
<pngx-page-header title="Users & Groups" i18n-title>
</pngx-page-header>
<ng-container *ngIf="users">
@if (users) {
<h4 class="d-flex">
<ng-container i18n>Users</ng-container>
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editUser()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }">
@ -20,8 +20,8 @@
<div class="col" i18n>Actions</div>
</div>
</li>
<li *ngFor="let user of users" class="list-group-item">
@for (user of users; track user) {
<li class="list-group-item">
<div class="row">
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editUser(user)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.User)">{{user.username}}</button></div>
<div class="col d-flex align-items-center">{{user.first_name}} {{user.last_name}}</div>
@ -42,10 +42,11 @@
</div>
</div>
</li>
}
</ul>
</ng-container>
}
<ng-container *ngIf="groups">
@if (groups) {
<h4 class="mt-4 d-flex">
<ng-container i18n>Groups</ng-container>
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editGroup()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Group }">
@ -55,7 +56,8 @@
<ng-container i18n>Add Group</ng-container>
</button>
</h4>
<ul *ngIf="groups.length > 0" class="list-group">
@if (groups.length > 0) {
<ul class="list-group">
<li class="list-group-item">
<div class="row">
<div class="col" i18n>Name</div>
@ -64,8 +66,8 @@
<div class="col" i18n>Actions</div>
</div>
</li>
<li *ngFor="let group of groups" class="list-group-item">
@for (group of groups; track group) {
<li class="list-group-item">
<div class="row">
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editGroup(group)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.Group)">{{group.name}}</button></div>
<div class="col"></div>
@ -86,12 +88,17 @@
</div>
</div>
</li>
<li *ngIf="groups.length === 0" class="list-group-item" i18n>No groups defined</li>
}
@if (groups.length === 0) {
<li class="list-group-item" i18n>No groups defined</li>
}
</ul>
}
}
</ng-container>
<div *ngIf="!users || !groups">
@if (!users || !groups) {
<div>
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div>
</div>
}

View File

@ -41,8 +41,8 @@ import { TextComponent } from '../../common/input/text/text.component'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { SettingsComponent } from '../settings/settings.component'
import { UsersAndGroupsComponent } from './users-groups.component'
import { PaperlessUser } from 'src/app/data/paperless-user'
import { PaperlessGroup } from 'src/app/data/paperless-group'
import { User } from 'src/app/data/user'
import { Group } from 'src/app/data/group'
const users = [
{ id: 1, username: 'user1', is_superuser: false },
@ -119,7 +119,7 @@ describe('UsersAndGroupsComponent', () => {
of({
all: users.map((a) => a.id),
count: users.length,
results: (users as PaperlessUser[]).concat([]),
results: (users as User[]).concat([]),
})
)
}
@ -128,7 +128,7 @@ describe('UsersAndGroupsComponent', () => {
of({
all: groups.map((r) => r.id),
count: groups.length,
results: (groups as PaperlessGroup[]).concat([]),
results: (groups as Group[]).concat([]),
})
)
}

View File

@ -1,8 +1,8 @@
import { Component, OnDestroy, OnInit } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { Subject, first, takeUntil } from 'rxjs'
import { PaperlessGroup } from 'src/app/data/paperless-group'
import { PaperlessUser } from 'src/app/data/paperless-user'
import { Group } from 'src/app/data/group'
import { User } from 'src/app/data/user'
import { PermissionsService } from 'src/app/services/permissions.service'
import { GroupService } from 'src/app/services/rest/group.service'
import { UserService } from 'src/app/services/rest/user.service'
@ -23,8 +23,8 @@ export class UsersAndGroupsComponent
extends ComponentWithPermissions
implements OnInit, OnDestroy
{
users: PaperlessUser[]
groups: PaperlessGroup[]
users: User[]
groups: Group[]
unsubscribeNotifier: Subject<any> = new Subject()
@ -69,7 +69,7 @@ export class UsersAndGroupsComponent
this.unsubscribeNotifier.next(true)
}
editUser(user: PaperlessUser = null) {
editUser(user: User = null) {
var modal = this.modalService.open(UserEditDialogComponent, {
backdrop: 'static',
size: 'xl',
@ -80,7 +80,7 @@ export class UsersAndGroupsComponent
modal.componentInstance.object = user
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((newUser: PaperlessUser) => {
.subscribe((newUser: User) => {
if (
newUser.id === this.settings.currentUser.id &&
(modal.componentInstance as UserEditDialogComponent).passwordIsSet
@ -107,7 +107,7 @@ export class UsersAndGroupsComponent
})
}
deleteUser(user: PaperlessUser) {
deleteUser(user: User) {
let modal = this.modalService.open(ConfirmDialogComponent, {
backdrop: 'static',
})
@ -133,7 +133,7 @@ export class UsersAndGroupsComponent
})
}
editGroup(group: PaperlessGroup = null) {
editGroup(group: Group = null) {
var modal = this.modalService.open(GroupEditDialogComponent, {
backdrop: 'static',
size: 'lg',
@ -157,7 +157,7 @@ export class UsersAndGroupsComponent
})
}
deleteGroup(group: PaperlessGroup) {
deleteGroup(group: Group) {
let modal = this.modalService.open(ConfirmDialogComponent, {
backdrop: 'static',
})

View File

@ -4,24 +4,32 @@
(click)="isMenuCollapsed = !isMenuCollapsed">
<span class="navbar-toggler-icon"></span>
</button>
<a class="navbar-brand col-auto col-md-3 col-lg-2 me-0 px-3 py-3 order-sm-0" [ngClass]="slimSidebarEnabled ? 'slim' : 'col-auto col-md-3 col-lg-2'" routerLink="/dashboard" tourAnchor="tour.intro">
<a class="navbar-brand col-auto col-md-3 col-lg-2 me-0 px-3 py-3 order-sm-0"
[ngClass]="slimSidebarEnabled ? 'slim' : 'col-auto col-md-3 col-lg-2'" routerLink="/dashboard"
tourAnchor="tour.intro">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.43 238.91" width="1em" class="me-2" fill="currentColor">
<path d="M194.7,0C164.22,70.94,17.64,79.74,64.55,194.06c.58,1.47-10.85,17-18.47,29.9-1.76-6.45-3.81-13.48-3.52-14.07,38.11-45.14-27.26-70.65-30.78-107.58C-4.64,131.62-10.5,182.92,39,212.53c.3,0,2.64,11.14,3.81,16.71a58.55,58.55,0,0,0-2.93,6.45c-1.17,2.93,7.62,2.64,7.62,3.22.88-.29,21.7-36.93,22.28-37.23C187.67,174.72,208.48,68.6,194.7,0ZM134.61,74.75C79.5,124,70.12,160.64,71.88,178.53,53.41,134.85,107.64,86.77,134.61,74.75ZM28.2,145.11c10.55,9.67,28.14,39.28,13.19,56.57C44.91,193.77,46.08,175.89,28.2,145.11Z" transform="translate(0 0)"/>
<path
d="M194.7,0C164.22,70.94,17.64,79.74,64.55,194.06c.58,1.47-10.85,17-18.47,29.9-1.76-6.45-3.81-13.48-3.52-14.07,38.11-45.14-27.26-70.65-30.78-107.58C-4.64,131.62-10.5,182.92,39,212.53c.3,0,2.64,11.14,3.81,16.71a58.55,58.55,0,0,0-2.93,6.45c-1.17,2.93,7.62,2.64,7.62,3.22.88-.29,21.7-36.93,22.28-37.23C187.67,174.72,208.48,68.6,194.7,0ZM134.61,74.75C79.5,124,70.12,160.64,71.88,178.53,53.41,134.85,107.64,86.77,134.61,74.75ZM28.2,145.11c10.55,9.67,28.14,39.28,13.19,56.57C44.91,193.77,46.08,175.89,28.2,145.11Z"
transform="translate(0 0)" />
</svg>
<span class="ms-2" [class.visually-hidden]="slimSidebarEnabled" i18n="app title">Paperless-ngx</span>
</a>
<div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 ps-md-4 me-sm-auto order-3 order-sm-1" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
<div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 ps-md-4 me-sm-auto order-3 order-sm-1"
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
<form (ngSubmit)="search()" class="form-inline flex-grow-1">
<svg width="1em" height="1em" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#search" />
</svg>
<input class="form-control form-control-sm" type="text" placeholder="Search documents" aria-label="Search"
[formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (keyup)="searchFieldKeyup($event)" (selectItem)="itemSelected($event)" i18n-placeholder>
<button type="button" *ngIf="!searchFieldEmpty" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0" (click)="resetSearchField()">
[formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (keyup)="searchFieldKeyup($event)"
(selectItem)="itemSelected($event)" i18n-placeholder>
@if (!searchFieldEmpty) {
<button type="button" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0" (click)="resetSearchField()">
<svg fill="currentColor" class="buttonicon-sm me-1">
<use xlink:href="assets/bootstrap-icons.svg#x" />
</svg>
</button>
}
</form>
</div>
<ul ngbNav class="order-sm-3">
@ -44,7 +52,8 @@
<use xlink:href="assets/bootstrap-icons.svg#person" />
</svg><ng-container i18n>My Profile</ng-container>
</button>
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }">
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()"
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }">
<svg class="sidebaricon me-2" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#gear" />
</svg><ng-container i18n>Settings</ng-container>
@ -55,7 +64,8 @@
</svg><ng-container i18n>Logout</ng-container>
</a>
<div class="dropdown-divider"></div>
<a ngbDropdownItem class="nav-link" target="_blank" rel="noopener noreferrer" href="https://docs.paperless-ngx.com">
<a ngbDropdownItem class="nav-link" target="_blank" rel="noopener noreferrer"
href="https://docs.paperless-ngx.com">
<svg class="sidebaricon me-2" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#question-circle" />
</svg><ng-container i18n>Documentation</ng-container>
@ -67,24 +77,33 @@
<div class="container-fluid">
<div class="row">
<nav id="sidebarMenu" class="d-md-block bg-light sidebar collapse" [ngClass]="slimSidebarEnabled ? 'slim' : 'col-md-3 col-lg-2 col-xxxl-1'" [class.animating]="slimSidebarAnimating" [ngbCollapse]="isMenuCollapsed">
<nav id="sidebarMenu" class="d-md-block bg-light sidebar collapse"
[ngClass]="slimSidebarEnabled ? 'slim' : 'col-md-3 col-lg-2 col-xxxl-1'" [class.animating]="slimSidebarAnimating"
[ngbCollapse]="isMenuCollapsed">
<button class="btn btn-sm btn-dark sidebar-slim-toggler" (click)="toggleSlimSidebar()">
<svg class="sidebaricon-sm" fill="currentColor">
<use *ngIf="slimSidebarEnabled" xlink:href="assets/bootstrap-icons.svg#chevron-double-right"/>
<use *ngIf="!slimSidebarEnabled" xlink:href="assets/bootstrap-icons.svg#chevron-double-left"/>
@if (slimSidebarEnabled) {
<use xlink:href="assets/bootstrap-icons.svg#chevron-double-right" />
} @else {
<use xlink:href="assets/bootstrap-icons.svg#chevron-double-left" />
}
</svg>
</button>
<div class="sidebar-sticky pt-3 d-flex flex-column justify-space-around">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" routerLink="dashboard" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Dashboard" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<a class="nav-link" routerLink="dashboard" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Dashboard" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#house" />
</svg><span>&nbsp;<ng-container i18n>Dashboard</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
<a class="nav-link" routerLink="documents" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Documents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<a class="nav-link" routerLink="documents" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Documents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#files" />
</svg><span>&nbsp;<ng-container i18n>Documents</ng-container></span>
@ -92,39 +111,52 @@
</li>
</ul>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
<h6 class="sidebar-heading px-3 mt-3 mb-1 text-muted" *ngIf='savedViewService.loading || savedViewService.sidebarViews?.length > 0'>
@if (savedViewService.loading || savedViewService.sidebarViews?.length > 0) {
<h6 class="sidebar-heading px-3 mt-3 mb-1 text-muted">
<span i18n>Saved views</span>
<div *ngIf="savedViewService.loading" class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div>
@if (savedViewService.loading) {
<div class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div>
}
</h6>
}
<ul class="nav flex-column mb-2" cdkDropList (cdkDropListDropped)="onDrop($event)">
<li class="nav-item w-100" *ngFor="let view of savedViewService.sidebarViews"
cdkDrag
[cdkDragDisabled]="!settingsService.organizingSidebarSavedViews"
cdkDragPreviewContainer="parent"
cdkDragPreviewClass="navItemDrag"
(cdkDragStarted)="onDragStart($event)"
@for (view of savedViewService.sidebarViews; track view) {
<li class="nav-item w-100" cdkDrag [cdkDragDisabled]="!settingsService.organizingSidebarSavedViews"
cdkDragPreviewContainer="parent" cdkDragPreviewClass="navItemDrag" (cdkDragStarted)="onDragStart($event)"
(cdkDragEnded)="onDragEnd($event)">
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="view/{{view.id}}" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name" [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="view/{{view.id}}"
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name"
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#funnel" />
</svg><span>&nbsp;{{view.name}}</span>
</a>
<div *ngIf="settingsService.organizingSidebarSavedViews" class="position-absolute end-0 top-0 px-3 py-2" [class.me-n3]="slimSidebarEnabled" cdkDragHandle>
@if (settingsService.organizingSidebarSavedViews) {
<div class="position-absolute end-0 top-0 px-3 py-2" [class.me-n3]="slimSidebarEnabled" cdkDragHandle>
<svg class="sidebaricon text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#grip-vertical" />
</svg>
</div>
}
</li>
}
</ul>
</ng-container>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
<h6 class="sidebar-heading px-3 mt-3 mb-1 text-muted" *ngIf='openDocuments.length > 0'>
@if (openDocuments.length > 0) {
<h6 class="sidebar-heading px-3 mt-3 mb-1 text-muted">
<span i18n>Open documents</span>
</h6>
}
<ul class="nav flex-column mb-2">
<li class="nav-item w-100" *ngFor='let d of openDocuments'>
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="documents/{{d.id}}" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="d.title | documentTitle" [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
@for (d of openDocuments; track d) {
<li class="nav-item w-100">
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="documents/{{d.id}}"
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="d.title | documentTitle"
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-text" />
</svg><span>&nbsp;{{d.title | documentTitle}}</span>
@ -135,13 +167,18 @@
</span>
</a>
</li>
<li class="nav-item w-100" *ngIf="openDocuments.length >= 1">
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" [routerLink]="[]" (click)="closeAll()" ngbPopover="Close all" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
}
@if (openDocuments.length >= 1) {
<li class="nav-item w-100">
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" [routerLink]="[]" (click)="closeAll()"
ngbPopover="Close all" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x" />
</svg><span>&nbsp;<ng-container i18n>Close all</ng-container></span>
</a>
</li>
}
</ul>
</ng-container>
@ -149,50 +186,70 @@
<span i18n>Manage</span>
</h6>
<ul class="nav flex-column mb-2">
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Correspondents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<li class="nav-item"
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Correspondents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person" />
</svg><span>&nbsp;<ng-container i18n>Correspondents</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }" tourAnchor="tour.tags">
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Tags" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"
tourAnchor="tour.tags">
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Tags"
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#tags" />
</svg><span>&nbsp;<ng-container i18n>Tags</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Document Types" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<li class="nav-item"
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Document Types" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#hash" />
</svg><span>&nbsp;<ng-container i18n>Document Types</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
<a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Storage Paths" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Storage Paths" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#folder" />
</svg><span>&nbsp;<ng-container i18n>Storage Paths</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
<a class="nav-link" routerLink="customfields" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Custom Fields" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<a class="nav-link" routerLink="customfields" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Custom Fields" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#ui-radios" />
</svg><span>&nbsp;<ng-container i18n>Custom Fields</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.ConsumptionTemplate }" tourAnchor="tour.consumption-templates">
<a class="nav-link" routerLink="templates" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Consumption templates" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<li class="nav-item"
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.ConsumptionTemplate }"
tourAnchor="tour.consumption-templates">
<a class="nav-link" routerLink="templates" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Consumption templates" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-earmark-ruled" />
</svg><span>&nbsp;<ng-container i18n>Templates</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailAccount }" tourAnchor="tour.mail">
<a class="nav-link" routerLink="mail" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Mail" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailAccount }"
tourAnchor="tour.mail">
<a class="nav-link" routerLink="mail" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Mail"
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#envelope" />
</svg><span>&nbsp;<ng-container i18n>Mail</ng-container></span>
@ -204,37 +261,55 @@
<span i18n>Administration</span>
</h6>
<ul class="nav flex-column mb-2">
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }" tourAnchor="tour.settings">
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Settings" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }"
tourAnchor="tour.settings">
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Settings" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#gear" />
</svg><span>&nbsp;<ng-container i18n>Settings</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
<a class="nav-link" routerLink="usersgroups" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Users & Groups" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<a class="nav-link" routerLink="usersgroups" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Users & Groups" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#people" />
</svg><span>&nbsp;<ng-container i18n>Users & Groups</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.PaperlessTask }" tourAnchor="tour.file-tasks">
<a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()" ngbPopover="File Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<span *ngIf="tasksService.failedFileTasks.length > 0 && slimSidebarEnabled" class="badge bg-danger position-absolute top-0 end-0">{{tasksService.failedFileTasks.length}}</span>
<li class="nav-item"
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.PaperlessTask }"
tourAnchor="tour.file-tasks">
<a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="File Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
@if (tasksService.failedFileTasks.length > 0 && slimSidebarEnabled) {
<span class="badge bg-danger position-absolute top-0 end-0">{{tasksService.failedFileTasks.length}}</span>
}
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#list-task" />
</svg><span>&nbsp;<ng-container i18n>File Tasks<span *ngIf="tasksService.failedFileTasks.length > 0"><span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span></span></ng-container></span>
</svg><span>&nbsp;<ng-container i18n>File Tasks@if (tasksService.failedFileTasks.length > 0) {
<span><span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span></span>
}</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }">
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Logs" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Logs"
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#text-left" />
</svg><span>&nbsp;<ng-container i18n>Logs</ng-container></span>
</a>
</li>
<li class="nav-item mt-2" tourAnchor="tour.outro">
<a class="px-3 py-2 text-muted small d-flex align-items-center flex-wrap text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://docs.paperless-ngx.com" ngbPopover="Documentation" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<a class="px-3 py-2 text-muted small d-flex align-items-center flex-wrap text-decoration-none"
target="_blank" rel="noopener noreferrer" href="https://docs.paperless-ngx.com" ngbPopover="Documentation"
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#question-circle" />
</svg><span class="ms-1">&nbsp;<ng-container i18n>Documentation</ng-container></span>
@ -243,13 +318,18 @@
<li class="nav-item" [class.visually-hidden]="slimSidebarEnabled">
<div class="px-3 py-0 text-muted small d-flex align-items-center flex-wrap">
<div class="me-3">
<a class="text-muted text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx" ngbPopover="GitHub" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<a class="text-muted text-decoration-none" target="_blank" rel="noopener noreferrer"
href="https://github.com/paperless-ngx/paperless-ngx" ngbPopover="GitHub" i18n-ngbPopover
[disablePopover]="!slimSidebarEnabled" placement="end" container="body"
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
{{ versionString }}
</a>
</div>
<div *ngIf="!settingsService.updateCheckingIsSet || appRemoteVersion" class="version-check">
@if (!settingsService.updateCheckingIsSet || appRemoteVersion) {
<div class="version-check">
<ng-template #updateAvailablePopContent>
<span class="small">Paperless-ngx {{ appRemoteVersion.version }} <ng-container i18n>is available.</ng-container><br/><ng-container i18n>Click to view.</ng-container></span>
<span class="small">Paperless-ngx {{ appRemoteVersion.version }} <ng-container i18n>is
available.</ng-container><br /><ng-container i18n>Click to view.</ng-container></span>
</ng-template>
<ng-template #updateCheckingNotEnabledPopContent>
<p class="small mb-2">
@ -265,31 +345,41 @@
</a>
</p>
</ng-template>
<ng-container *ngIf="settingsService.updateCheckingIsSet; else updateCheckNotSet">
<a *ngIf="appRemoteVersion.update_available" class="small text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx/releases"
[ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave" container="body">
<svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16">
@if (settingsService.updateCheckingIsSet) {
@if (appRemoteVersion.update_available) {
<a class="small text-decoration-none" target="_blank" rel="noopener noreferrer"
href="https://github.com/paperless-ngx/paperless-ngx/releases"
[ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave"
container="body">
<svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;"
viewBox="0 0 16 16">
<use xlink:href="assets/bootstrap-icons.svg#info-circle" />
</svg>
<ng-container *ngIf="appRemoteVersion?.update_available" i18n>Update available</ng-container>
@if (appRemoteVersion?.update_available) {
<ng-container i18n>Update available</ng-container>
}
</a>
</ng-container>
<ng-template #updateCheckNotSet>
}
} @else {
<a class="small text-decoration-none" routerLink="/settings" fragment="update-checking"
[ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter" container="body">
<svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16">
[ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter"
container="body">
<svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;"
viewBox="0 0 16 16">
<use xlink:href="assets/bootstrap-icons.svg#info-circle" />
</svg>
</a>
</ng-template>
}
</div>
}
</div>
</li>
</ul>
</div>
</nav>
<main role="main" class="ms-sm-auto px-md-4" [ngClass]="slimSidebarEnabled ? 'col-slim' : 'col-md-9 col-lg-10 col-xxxl-11'">
<main role="main" class="ms-sm-auto px-md-4"
[ngClass]="slimSidebarEnabled ? 'col-slim' : 'col-md-9 col-lg-10 col-xxxl-11'">
<router-outlet></router-outlet>
</main>
</div>

View File

@ -15,11 +15,11 @@ 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 { SETTINGS_KEYS } from 'src/app/data/ui-settings'
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 { Observable, of, tap, throwError } from 'rxjs'
import { of, throwError } 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'
@ -31,7 +31,7 @@ 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'
import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop'
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
import { SavedView } from 'src/app/data/saved-view'
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
const saved_views = [
@ -356,7 +356,7 @@ describe('AppFrameComponent', () => {
const toastSpy = jest.spyOn(toastService, 'showInfo')
jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true))
component.onDrop({ previousIndex: 0, currentIndex: 1 } as CdkDragDrop<
PaperlessSavedView[]
SavedView[]
>)
expect(settingsSpy).toHaveBeenCalledWith([
saved_views[2],
@ -379,7 +379,7 @@ describe('AppFrameComponent', () => {
.spyOn(settingsService, 'storeSettings')
.mockReturnValue(throwError(() => new Error('unable to save')))
component.onDrop({ previousIndex: 0, currentIndex: 2 } as CdkDragDrop<
PaperlessSavedView[]
SavedView[]
>)
expect(toastSpy).toHaveBeenCalled()
})

View File

@ -10,7 +10,7 @@ import {
first,
catchError,
} from 'rxjs/operators'
import { PaperlessDocument } from 'src/app/data/paperless-document'
import { Document } from 'src/app/data/document'
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { SearchService } from 'src/app/services/rest/search.service'
@ -25,7 +25,7 @@ import {
import { SettingsService } from 'src/app/services/settings.service'
import { TasksService } from 'src/app/services/tasks.service'
import { ComponentCanDeactivate } from 'src/app/guards/dirty-doc.guard'
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { ToastService } from 'src/app/services/toast.service'
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
import {
@ -33,7 +33,7 @@ import {
PermissionsService,
PermissionType,
} from 'src/app/services/permissions.service'
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
import { SavedView } from 'src/app/data/saved-view'
import {
CdkDragStart,
CdkDragEnd,
@ -132,7 +132,7 @@ export class AppFrameComponent
this.closeMenu()
}
get openDocuments(): PaperlessDocument[] {
get openDocuments(): Document[] {
return this.openDocumentsService.getOpenDocuments()
}
@ -200,7 +200,7 @@ export class AppFrameComponent
])
}
closeDocument(d: PaperlessDocument) {
closeDocument(d: Document) {
this.openDocumentsService
.closeDocument(d)
.pipe(first())
@ -250,7 +250,7 @@ export class AppFrameComponent
this.settingsService.globalDropzoneEnabled = true
}
onDrop(event: CdkDragDrop<PaperlessSavedView[]>) {
onDrop(event: CdkDragDrop<SavedView[]>) {
const sidebarViews = this.savedViewService.sidebarViews.concat([])
moveItemInArray(sidebarViews, event.previousIndex, event.currentIndex)

View File

@ -1,9 +1,15 @@
<button *ngIf="active" class="position-absolute top-0 start-100 translate-middle badge bg-secondary border border-light rounded-pill p-1" title="Clear" i18n-title (click)="onClick($event)">
<svg *ngIf="!isNumbered && selected" width="1em" height="1em" class="check m-0 p-0 opacity-75" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
@if (active) {
<button class="position-absolute top-0 start-100 translate-middle badge bg-secondary border border-light rounded-pill p-1" title="Clear" i18n-title (click)="onClick($event)">
@if (!isNumbered && selected) {
<svg width="1em" height="1em" class="check m-0 p-0 opacity-75" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<use xlink:href="assets/bootstrap-icons.svg#check-lg"/>
</svg>
<div *ngIf="isNumbered" class="number">{{number}}<span class="visually-hidden">selected</span></div>
}
@if (isNumbered) {
<div class="number">{{number}}<span class="visually-hidden">selected</span></div>
}
<svg width=".9em" height="1em" class="x m-0 p-0 opacity-75" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<use xlink:href="assets/bootstrap-icons.svg#x-lg"/>
</svg>
</button>
}

View File

@ -4,8 +4,12 @@
</button>
</div>
<div class="modal-body">
<p *ngIf="messageBold"><b>{{messageBold}}</b></p>
<p class="mb-0" *ngIf="message" [innerHTML]="message | safeHtml"></p>
@if (messageBold) {
<p><b>{{messageBold}}</b></p>
}
@if (message) {
<p class="mb-0" [innerHTML]="message | safeHtml"></p>
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" [disabled]="!buttonsEnabled" i18n>
@ -16,9 +20,13 @@
{{btnCaption}}
<span class="visually-hidden">{{ seconds | number: '1.0-0' }} seconds</span>
</span>
<ngb-progressbar *ngIf="!confirmButtonEnabled" style="height: 1px;" type="dark" [max]="secondsTotal" [value]="seconds"></ngb-progressbar>
@if (!confirmButtonEnabled) {
<ngb-progressbar style="height: 1px;" type="dark" [max]="secondsTotal" [value]="seconds"></ngb-progressbar>
}
</button>
<button *ngIf="alternativeBtnCaption" type="button" class="btn" [class]="alternativeBtnClass" (click)="alternative()" [disabled]="!alternativeButtonEnabled || !buttonsEnabled">
@if (alternativeBtnCaption) {
<button type="button" class="btn" [class]="alternativeBtnClass" (click)="alternative()" [disabled]="!alternativeButtonEnabled || !buttonsEnabled">
{{alternativeBtnCaption}}
</button>
}
</div>

View File

@ -8,10 +8,7 @@ import {
import { ToastService } from 'src/app/services/toast.service'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { of } from 'rxjs'
import {
PaperlessCustomField,
PaperlessCustomFieldDataType,
} from 'src/app/data/paperless-custom-field'
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
import { SelectComponent } from '../input/select/select.component'
import { NgSelectModule } from '@ng-select/ng-select'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
@ -24,16 +21,16 @@ import {
import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import { By } from '@angular/platform-browser'
const fields: PaperlessCustomField[] = [
const fields: CustomField[] = [
{
id: 0,
name: 'Field 1',
data_type: PaperlessCustomFieldDataType.Integer,
data_type: CustomFieldDataType.Integer,
},
{
id: 1,
name: 'Field 2',
data_type: PaperlessCustomFieldDataType.String,
data_type: CustomFieldDataType.String,
},
]

View File

@ -7,8 +7,8 @@ import {
} from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { Subject, first, takeUntil } from 'rxjs'
import { PaperlessCustomField } from 'src/app/data/paperless-custom-field'
import { PaperlessCustomFieldInstance } from 'src/app/data/paperless-custom-field-instance'
import { CustomField } from 'src/app/data/custom-field'
import { CustomFieldInstance } from 'src/app/data/custom-field-instance'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { ToastService } from 'src/app/services/toast.service'
import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
@ -31,16 +31,16 @@ export class CustomFieldsDropdownComponent implements OnDestroy {
disabled: boolean = false
@Input()
existingFields: PaperlessCustomFieldInstance[] = []
existingFields: CustomFieldInstance[] = []
@Output()
added: EventEmitter<PaperlessCustomField> = new EventEmitter()
added: EventEmitter<CustomField> = new EventEmitter()
@Output()
created: EventEmitter<PaperlessCustomField> = new EventEmitter()
created: EventEmitter<CustomField> = new EventEmitter()
private customFields: PaperlessCustomField[] = []
public unusedFields: PaperlessCustomField[]
private customFields: CustomField[] = []
public unusedFields: CustomField[]
public name: string
@ -88,8 +88,8 @@ export class CustomFieldsDropdownComponent implements OnDestroy {
}
public getCustomFieldFromInstance(
instance: PaperlessCustomFieldInstance
): PaperlessCustomField {
instance: CustomFieldInstance
): CustomField {
return this.customFields.find((f) => f.id === instance.field)
}

View File

@ -5,11 +5,14 @@
</button>
<div class="dropdown-menu date-dropdown shadow pt-0" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
<div class="list-group list-group-flush">
<button *ngFor="let rd of relativeDates" class="list-group-item small list-goup list-group-item-action d-flex p-2" role="menuitem" (click)="setRelativeDate(rd.id)">
@for (rd of relativeDates; track rd) {
<button class="list-group-item small list-goup list-group-item-action d-flex p-2" role="menuitem" (click)="setRelativeDate(rd.id)">
<div class="selected-icon">
<svg *ngIf="relativeDate === rd.id" fill="currentColor" class="buttonicon-sm">
@if (relativeDate === rd.id) {
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
}
</div>
<div class="d-flex justify-content-between w-100 align-items-center ps-2">
<div class="pe-2 pe-lg-4">
@ -22,16 +25,19 @@
</div>
</div>
</button>
}
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
<div class="mb-2 d-flex flex-row w-100 justify-content-between small">
<div i18n>After</div>
<a *ngIf="dateAfter" class="btn btn-link p-0 m-0" (click)="clearAfter()">
@if (dateAfter) {
<a class="btn btn-link p-0 m-0" (click)="clearAfter()">
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
<small i18n>Clear</small>
</a>
}
</div>
<div class="input-group input-group-sm">
@ -49,12 +55,14 @@
<div class="mb-2 d-flex flex-row w-100 justify-content-between small">
<div i18n>Before</div>
<a *ngIf="dateBefore" class="btn btn-link p-0 m-0" (click)="clearBefore()">
@if (dateBefore) {
<a class="btn btn-link p-0 m-0" (click)="clearBefore()">
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
<small i18n>Clear</small>
</a>
}
</div>
<div class="input-group input-group-sm">

View File

@ -86,7 +86,9 @@
</div>
</div>
<div class="modal-footer">
<span class="text-danger" *ngIf="error?.non_field_errors"><ng-container i18n>Error</ng-container>: {{error.non_field_errors}}</span>
@if (error?.non_field_errors) {
<span class="text-danger"><ng-container i18n>Error</ng-container>: {{error.non_field_errors}}</span>
}
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
</div>

View File

@ -4,11 +4,11 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { first } from 'rxjs'
import {
DocumentSource,
PaperlessConsumptionTemplate,
} from 'src/app/data/paperless-consumption-template'
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
ConsumptionTemplate,
} from 'src/app/data/consumption-template'
import { Correspondent } from 'src/app/data/correspondent'
import { DocumentType } from 'src/app/data/document-type'
import { StoragePath } from 'src/app/data/storage-path'
import { ConsumptionTemplateService } from 'src/app/services/rest/consumption-template.service'
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
@ -17,9 +17,9 @@ import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
import { EditDialogComponent } from '../edit-dialog.component'
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
import { PaperlessMailRule } from 'src/app/data/paperless-mail-rule'
import { MailRule } from 'src/app/data/mail-rule'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { PaperlessCustomField } from 'src/app/data/paperless-custom-field'
import { CustomField } from 'src/app/data/custom-field'
export const DOCUMENT_SOURCE_OPTIONS = [
{
@ -41,13 +41,13 @@ export const DOCUMENT_SOURCE_OPTIONS = [
templateUrl: './consumption-template-edit-dialog.component.html',
styleUrls: ['./consumption-template-edit-dialog.component.scss'],
})
export class ConsumptionTemplateEditDialogComponent extends EditDialogComponent<PaperlessConsumptionTemplate> {
templates: PaperlessConsumptionTemplate[]
correspondents: PaperlessCorrespondent[]
documentTypes: PaperlessDocumentType[]
storagePaths: PaperlessStoragePath[]
mailRules: PaperlessMailRule[]
customFields: PaperlessCustomField[]
export class ConsumptionTemplateEditDialogComponent extends EditDialogComponent<ConsumptionTemplate> {
templates: ConsumptionTemplate[]
correspondents: Correspondent[]
documentTypes: DocumentType[]
storagePaths: StoragePath[]
mailRules: MailRule[]
customFields: CustomField[]
constructor(
service: ConsumptionTemplateService,

View File

@ -8,8 +8,12 @@
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
<pngx-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
<pngx-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive" novalidate></pngx-input-check>
@if (patternRequired) {
<pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
}
@if (patternRequired) {
<pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" novalidate></pngx-input-check>
}
<div *pngxIfOwner="object">
<pngx-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></pngx-permissions-form>

View File

@ -3,7 +3,7 @@ import { FormControl, FormGroup } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
import { Correspondent } from 'src/app/data/correspondent'
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
@ -13,7 +13,7 @@ import { SettingsService } from 'src/app/services/settings.service'
templateUrl: './correspondent-edit-dialog.component.html',
styleUrls: ['./correspondent-edit-dialog.component.scss'],
})
export class CorrespondentEditDialogComponent extends EditDialogComponent<PaperlessCorrespondent> {
export class CorrespondentEditDialogComponent extends EditDialogComponent<Correspondent> {
constructor(
service: CorrespondentService,
activeModal: NgbActiveModal,

View File

@ -7,7 +7,9 @@
<div class="modal-body">
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
<pngx-input-select i18n-title title="Data type" [items]="getDataTypes()" formControlName="data_type"></pngx-input-select>
<small class="d-block mt-n2" *ngIf="typeFieldDisabled" i18n>Data type cannot be changed after a field is created</small>
@if (typeFieldDisabled) {
<small class="d-block mt-n2" i18n>Data type cannot be changed after a field is created</small>
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>

View File

@ -1,10 +1,7 @@
import { Component, OnInit } from '@angular/core'
import { FormGroup, FormControl } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import {
DATA_TYPE_LABELS,
PaperlessCustomField,
} from 'src/app/data/paperless-custom-field'
import { DATA_TYPE_LABELS, CustomField } from 'src/app/data/custom-field'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
@ -16,7 +13,7 @@ import { EditDialogComponent, EditDialogMode } from '../edit-dialog.component'
styleUrls: ['./custom-field-edit-dialog.component.scss'],
})
export class CustomFieldEditDialogComponent
extends EditDialogComponent<PaperlessCustomField>
extends EditDialogComponent<CustomField>
implements OnInit
{
constructor(

View File

@ -9,8 +9,12 @@
<div class="col">
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
<pngx-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
<pngx-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check>
@if (patternRequired) {
<pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
}
@if (patternRequired) {
<pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check>
}
</div>
<div *pngxIfOwner="object">

View File

@ -3,7 +3,7 @@ import { FormControl, FormGroup } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
import { DocumentType } from 'src/app/data/document-type'
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
@ -13,7 +13,7 @@ import { SettingsService } from 'src/app/services/settings.service'
templateUrl: './document-type-edit-dialog.component.html',
styleUrls: ['./document-type-edit-dialog.component.scss'],
})
export class DocumentTypeEditDialogComponent extends EditDialogComponent<PaperlessDocumentType> {
export class DocumentTypeEditDialogComponent extends EditDialogComponent<DocumentType> {
constructor(
service: DocumentTypeService,
activeModal: NgbActiveModal,

View File

@ -23,8 +23,8 @@ import {
MATCH_NONE,
MATCH_ALL,
} from 'src/app/data/matching-model'
import { PaperlessTag } from 'src/app/data/paperless-tag'
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
import { Tag } from 'src/app/data/tag'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
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'
@ -38,7 +38,7 @@ import { EditDialogComponent, EditDialogMode } from './edit-dialog.component'
</div>
`,
})
class TestComponent extends EditDialogComponent<PaperlessTag> {
class TestComponent extends EditDialogComponent<Tag> {
constructor(
service: TagService,
activeModal: NgbActiveModal,

View File

@ -9,12 +9,12 @@ import {
} from 'src/app/data/matching-model'
import { ObjectWithId } from 'src/app/data/object-with-id'
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
import { PaperlessUser } from 'src/app/data/paperless-user'
import { User } from 'src/app/data/user'
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'
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'
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
export enum EditDialogMode {
CREATE = 0,
@ -33,7 +33,7 @@ export abstract class EditDialogComponent<
private settingsService: SettingsService
) {}
users: PaperlessUser[]
users: User[]
@Input()
dialogMode: EditDialogMode = EditDialogMode.CREATE

View File

@ -2,7 +2,7 @@ import { Component } from '@angular/core'
import { FormControl, FormGroup } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
import { PaperlessGroup } from 'src/app/data/paperless-group'
import { Group } from 'src/app/data/group'
import { GroupService } from 'src/app/services/rest/group.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
@ -12,7 +12,7 @@ import { SettingsService } from 'src/app/services/settings.service'
templateUrl: './group-edit-dialog.component.html',
styleUrls: ['./group-edit-dialog.component.scss'],
})
export class GroupEditDialogComponent extends EditDialogComponent<PaperlessGroup> {
export class GroupEditDialogComponent extends EditDialogComponent<Group> {
constructor(
service: GroupService,
activeModal: NgbActiveModal,

View File

@ -22,13 +22,15 @@
</div>
<div class="modal-footer">
<div class="m-0 me-2">
<ngb-alert #testResultAlert *ngIf="testResult" [type]="testResult" class="mb-0 py-2" (closed)="testResult = null">{{testResultMessage}}</ngb-alert>
@if (testResult) {
<ngb-alert #testResultAlert [type]="testResult" class="mb-0 py-2" (closed)="testResult = null">{{testResultMessage}}</ngb-alert>
}
</div>
<button type="button" class="btn btn-outline-primary" (click)="test()" [disabled]="networkActive || testActive">
<ng-container *ngIf="testActive">
@if (testActive) {
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<span class="visually-hidden mr-1" i18n>Loading...</span>
</ng-container>
}
<ng-container i18n>Test</ng-container>
</button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>

View File

@ -11,7 +11,7 @@ import {
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select'
import { IMAPSecurity } from 'src/app/data/paperless-mail-account'
import { IMAPSecurity } from 'src/app/data/mail-account'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SettingsService } from 'src/app/services/settings.service'

View File

@ -2,10 +2,7 @@ import { Component, ViewChild } from '@angular/core'
import { FormControl, FormGroup } from '@angular/forms'
import { NgbActiveModal, NgbAlert } from '@ng-bootstrap/ng-bootstrap'
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
import {
IMAPSecurity,
PaperlessMailAccount,
} from 'src/app/data/paperless-mail-account'
import { IMAPSecurity, MailAccount } from 'src/app/data/mail-account'
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
@ -21,7 +18,7 @@ const IMAP_SECURITY_OPTIONS = [
templateUrl: './mail-account-edit-dialog.component.html',
styleUrls: ['./mail-account-edit-dialog.component.scss'],
})
export class MailAccountEditDialogComponent extends EditDialogComponent<PaperlessMailAccount> {
export class MailAccountEditDialogComponent extends EditDialogComponent<MailAccount> {
testActive: boolean = false
testResult: string
alertTimeout

View File

@ -26,19 +26,25 @@
</div>
<div class="col-md-4">
<pngx-input-select i18n-title title="Action" [items]="actionOptions" formControlName="action" i18n-hint hint="Action is only performed when documents are consumed from the mail. Mails without attachments remain entirely untouched."></pngx-input-select>
<pngx-input-text i18n-title title="Action parameter" *ngIf="showActionParamField" formControlName="action_parameter" [error]="error?.action_parameter"></pngx-input-text>
@if (showActionParamField) {
<pngx-input-text i18n-title title="Action parameter" formControlName="action_parameter" [error]="error?.action_parameter"></pngx-input-text>
}
<p class="small fst-italic mt-5" i18n>Assignments specified here will supersede any consumption templates.</p>
<pngx-input-select i18n-title title="Assign title from" [items]="metadataTitleOptions" formControlName="assign_title_from"></pngx-input-select>
<pngx-input-tags [allowCreate]="false" formControlName="assign_tags"></pngx-input-tags>
<pngx-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select>
<pngx-input-select i18n-title title="Assign correspondent from" [items]="metadataCorrespondentOptions" formControlName="assign_correspondent_from"></pngx-input-select>
<pngx-input-select *ngIf="showCorrespondentField" i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></pngx-input-select>
@if (showCorrespondentField) {
<pngx-input-select i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></pngx-input-select>
}
<pngx-input-check i18n-title title="Assign owner from rule" formControlName="assign_owner_from_rule"></pngx-input-check>
</div>
</div>
</div>
<div class="modal-footer">
<span class="text-danger" *ngIf="error?.non_field_errors"><ng-container i18n>Error</ng-container>: {{error.non_field_errors}}</span>
@if (error?.non_field_errors) {
<span class="text-danger"><ng-container i18n>Error</ng-container>: {{error.non_field_errors}}</span>
}
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
</div>

View File

@ -7,7 +7,7 @@ import { of } from 'rxjs'
import {
MailMetadataCorrespondentOption,
MailAction,
} from 'src/app/data/paperless-mail-rule'
} from 'src/app/data/mail-rule'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'

View File

@ -3,17 +3,17 @@ import { FormControl, FormGroup } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { first } from 'rxjs'
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
import { PaperlessMailAccount } from 'src/app/data/paperless-mail-account'
import { Correspondent } from 'src/app/data/correspondent'
import { DocumentType } from 'src/app/data/document-type'
import { MailAccount } from 'src/app/data/mail-account'
import {
MailAction,
MailFilterAttachmentType,
MailMetadataCorrespondentOption,
MailMetadataTitleOption,
PaperlessMailRule,
MailRule,
MailRuleConsumptionScope,
} from 'src/app/data/paperless-mail-rule'
} from 'src/app/data/mail-rule'
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
@ -109,10 +109,10 @@ const METADATA_CORRESPONDENT_OPTIONS = [
templateUrl: './mail-rule-edit-dialog.component.html',
styleUrls: ['./mail-rule-edit-dialog.component.scss'],
})
export class MailRuleEditDialogComponent extends EditDialogComponent<PaperlessMailRule> {
accounts: PaperlessMailAccount[]
correspondents: PaperlessCorrespondent[]
documentTypes: PaperlessDocumentType[]
export class MailRuleEditDialogComponent extends EditDialogComponent<MailRule> {
accounts: MailAccount[]
correspondents: Correspondent[]
documentTypes: DocumentType[]
constructor(
service: MailRuleService,

View File

@ -9,8 +9,12 @@
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
<pngx-input-text i18n-title title="Path" formControlName="path" [error]="error?.path" [hint]="pathHint"></pngx-input-text>
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
<pngx-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
<pngx-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check>
@if (patternRequired) {
<pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
}
@if (patternRequired) {
<pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check>
}
<div *pngxIfOwner="object">
<pngx-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></pngx-permissions-form>

View File

@ -3,7 +3,7 @@ import { FormControl, FormGroup } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
import { StoragePath } from 'src/app/data/storage-path'
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
@ -13,7 +13,7 @@ import { SettingsService } from 'src/app/services/settings.service'
templateUrl: './storage-path-edit-dialog.component.html',
styleUrls: ['./storage-path-edit-dialog.component.scss'],
})
export class StoragePathEditDialogComponent extends EditDialogComponent<PaperlessStoragePath> {
export class StoragePathEditDialogComponent extends EditDialogComponent<StoragePath> {
constructor(
service: StoragePathService,
activeModal: NgbActiveModal,

View File

@ -11,8 +11,12 @@
<pngx-input-check i18n-title title="Inbox tag" formControlName="is_inbox_tag" i18n-hint hint="Inbox tags are automatically assigned to all consumed documents."></pngx-input-check>
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
<pngx-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
<pngx-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check>
@if (patternRequired) {
<pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
}
@if (patternRequired) {
<pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check>
}
<div *pngxIfOwner="object">
<pngx-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></pngx-permissions-form>

View File

@ -2,7 +2,7 @@ import { Component } from '@angular/core'
import { FormControl, FormGroup } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
import { PaperlessTag } from 'src/app/data/paperless-tag'
import { Tag } from 'src/app/data/tag'
import { TagService } from 'src/app/services/rest/tag.service'
import { randomColor } from 'src/app/utils/color'
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
@ -14,7 +14,7 @@ import { SettingsService } from 'src/app/services/settings.service'
templateUrl: './tag-edit-dialog.component.html',
styleUrls: ['./tag-edit-dialog.component.scss'],
})
export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> {
export class TagEditDialogComponent extends EditDialogComponent<Tag> {
constructor(
service: TagService,
activeModal: NgbActiveModal,

View File

@ -3,8 +3,8 @@ import { FormControl, FormGroup } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { first } from 'rxjs'
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
import { PaperlessGroup } from 'src/app/data/paperless-group'
import { PaperlessUser } from 'src/app/data/paperless-user'
import { Group } from 'src/app/data/group'
import { User } from 'src/app/data/user'
import { GroupService } from 'src/app/services/rest/group.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
@ -15,10 +15,10 @@ import { SettingsService } from 'src/app/services/settings.service'
styleUrls: ['./user-edit-dialog.component.scss'],
})
export class UserEditDialogComponent
extends EditDialogComponent<PaperlessUser>
extends EditDialogComponent<User>
implements OnInit
{
groups: PaperlessGroup[]
groups: Group[]
passwordIsSet: boolean = false
constructor(

View File

@ -4,13 +4,14 @@
<use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
</svg>
<div class="d-none d-sm-inline">&nbsp;{{title}}</div>
<ng-container *ngIf="!editing && selectionModel.totalCount > 0">
@if (!editing && selectionModel.totalCount > 0) {
<pngx-clearable-badge [number]="selectionModel.totalCount" [selected]="selectionModel.selectionSize() > 0" (cleared)="reset()"></pngx-clearable-badge>
</ng-container>
}
</button>
<div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown_{{name}}">
<div class="list-group list-group-flush">
<div *ngIf="!editing && manyToOne" class="list-group-item d-flex">
@if (!editing && manyToOne) {
<div class="list-group-item d-flex">
<div class="btn-group btn-group-xs flex-fill" role="group">
<input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorAnd_{{name}}" name="logicalOperatorAnd_{{name}}" value="and">
<label class="btn btn-outline-primary" for="logicalOperatorAnd_{{name}}" i18n>All</label>
@ -18,7 +19,9 @@
<label class="btn btn-outline-primary" for="logicalOperatorOr_{{name}}" i18n>Any</label>
</div>
</div>
<div *ngIf="!editing && !manyToOne" class="list-group-item d-flex">
}
@if (!editing && !manyToOne) {
<div class="list-group-item d-flex">
<div class="btn-group btn-group-xs flex-fill" role="group">
<input [(ngModel)]="selectionModel.intersection" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleIntersection()" type="radio" class="btn-check" id="intersectionInclude_{{name}}" name="intersectionInclude_{{name}}" value="include">
<label class="btn btn-outline-primary" for="intersectionInclude_{{name}}" i18n>Include</label>
@ -26,27 +29,36 @@
<label class="btn btn-outline-primary" for="intersectionExclude_{{name}}" i18n>Exclude</label>
</div>
</div>
}
<div class="list-group-item">
<div class="input-group input-group-sm">
<input class="form-control" type="text" [(ngModel)]="filterText" [placeholder]="filterPlaceholder" (keyup.enter)="listFilterEnter()" #listFilterTextInput>
</div>
</div>
<div *ngIf="selectionModel.items" class="items" #buttonItems>
<ng-container *ngFor="let item of selectionModel.itemsSorted | filter: filterText; let i = index">
@if (selectionModel.items) {
<div class="items" #buttonItems>
@for (item of selectionModel.itemsSorted | filter: filterText; track item; let i = $index) {
@if (allowSelectNone || item.id) {
<pngx-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 - 1)" [disabled]="disabled">
[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">
</pngx-toggleable-dropdown-button>
</ng-container>
}
}
</div>
<button *ngIf="editing" class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!modelIsDirty || disabled">
}
@if (editing) {
<button class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!modelIsDirty || disabled">
<small class="ms-2" [ngClass]="{'fw-bold': modelIsDirty}" i18n>Apply</small>
<svg width="1.5em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#arrow-right" />
</svg>
</button>
<div *ngIf="!editing && manyToOne" class="list-group-item list-group-item-note pt-1 pb-2">
}
@if (!editing && manyToOne) {
<div class="list-group-item list-group-item-note pt-1 pb-2">
<small i18n>Click again to exclude items.</small>
</div>
}
</div>
</div>
</div>

View File

@ -13,7 +13,7 @@ import {
} 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 { Tag } from 'src/app/data/tag'
import {
DEFAULT_MATCHING_ALGORITHM,
MATCH_ALL,
@ -26,7 +26,7 @@ import { TagComponent } from '../tag/tag.component'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
const items: PaperlessTag[] = [
const items: Tag[] = [
{
id: 1,
name: 'Tag1',

View File

@ -1,24 +1,29 @@
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="toggleItem($event)" [disabled]="disabled">
<div class="selected-icon me-1">
<ng-container *ngIf="isChecked()">
@if (isChecked()) {
<svg fill="currentColor" class="buttonicon-sm bi-check">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
</ng-container>
<ng-container *ngIf="isPartiallyChecked()">
}
@if (isPartiallyChecked()) {
<svg fill="currentColor" class="buttonicon-sm bi-dash">
<use xlink:href="assets/bootstrap-icons.svg#dash"/>
</svg>
</ng-container>
<ng-container *ngIf="isExcluded()">
}
@if (isExcluded()) {
<svg fill="currentColor" class="buttonicon-sm bi-x">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
</ng-container>
}
</div>
<div class="me-1">
<pngx-tag *ngIf="isTag; else displayName" [tag]="item" [clickable]="false"></pngx-tag>
<ng-template #displayName><small>{{item.name}}</small></ng-template>
@if (isTag) {
<pngx-tag [tag]="item" [clickable]="false"></pngx-tag>
} @else {
<small>{{item.name}}</small>
}
</div>
<div *ngIf="!hideCount" class="badge bg-light text-dark rounded-pill ms-auto me-1">{{count ?? item.document_count}}</div>
@if (!hideCount) {
<div class="badge bg-light text-dark rounded-pill ms-auto me-1">{{count ?? item.document_count}}</div>
}
</button>

View File

@ -4,7 +4,7 @@ import {
ToggleableItemState,
} from './toggleable-dropdown-button.component'
import { TagComponent } from '../../tag/tag.component'
import { PaperlessTag } from 'src/app/data/paperless-tag'
import { Tag } from 'src/app/data/tag'
describe('ToggleableDropdownButtonComponent', () => {
let component: ToggleableDropdownButtonComponent
@ -26,7 +26,7 @@ describe('ToggleableDropdownButtonComponent', () => {
id: 1,
name: 'Test Tag',
is_inbox_tag: false,
} as PaperlessTag
} as Tag
fixture.detectChanges()
expect(component.isTag).toBeTruthy()

View File

@ -1,18 +1,26 @@
<div class="mb-3">
<div class="row">
<div *ngIf="horizontal" class="d-flex align-items-center position-relative hidden-button-container col-md-3">
@if (horizontal) {
<div class="d-flex align-items-center position-relative hidden-button-container col-md-3">
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>
}
<div [ngClass]="{'col-md-9': horizontal, 'align-items-center': horizontal, 'd-flex': horizontal}">
<div class="form-check">
<input #inputField type="checkbox" class="form-check-input" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()" [disabled]="disabled">
<label *ngIf="!horizontal" class="form-check-label" [for]="inputId">{{title}}</label>
<div *ngIf="hint" class="form-text text-muted">{{hint}}</div>
@if (!horizontal) {
<label class="form-check-label" [for]="inputId">{{title}}</label>
}
@if (hint) {
<div class="form-text text-muted">{{hint}}</div>
}
</div>
</div>
</div>

View File

@ -1,5 +1,7 @@
<div class="mb-3">
<label *ngIf="title" [for]="inputId">{{title}}</label>
@if (title) {
<label [for]="inputId">{{title}}</label>
}
<div class="input-group" [class.is-invalid]="error">
<span class="input-group-text" [style.background-color]="value">&nbsp;&nbsp;&nbsp;</span>
@ -21,7 +23,9 @@
</button>
</div>
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
@if (hint) {
<small class="form-text text-muted">{{hint}}</small>
}
<div class="invalid-feedback">
{{error}}
</div>

View File

@ -2,11 +2,13 @@
<div class="row">
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>
<div class="position-relative" [class.col-md-9]="horizontal">
<div class="input-group" [class.is-invalid]="error">
@ -18,20 +20,26 @@
<use _ngcontent-ng-c3750736003="" xlink:href="assets/bootstrap-icons.svg#calendar"></use>
</svg>
</button>
<button *ngIf="showFilter" class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="this.value === null" title="{{ fitlerButtonTitle }}">
@if (showFilter) {
<button class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="this.value === null" title="{{ filterButtonTitle }}">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#filter" />
</svg>
</button>
}
</div>
<div class="invalid-feedback position-absolute top-100" i18n>Invalid date.</div>
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
<small *ngIf="getSuggestions().length > 0">
@if (hint) {
<small class="form-text text-muted">{{hint}}</small>
}
@if (getSuggestions().length > 0) {
<small>
<span i18n>Suggestions:</span>&nbsp;
<ng-container *ngFor="let s of getSuggestions()">
@for (s of getSuggestions(); track s) {
<a (click)="onSuggestionClick(s)" [routerLink]="[]">{{s}}</a>&nbsp;
</ng-container>
}
</small>
}
</div>
</div>
</div>

View File

@ -1,12 +1,16 @@
<div class="mb-3 paperless-input-select" [class.disabled]="disabled">
<div class="row">
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
<label *ngIf="title" class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
@if (title) {
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
}
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>
<div [class.col-md-9]="horizontal">
<div>
@ -44,7 +48,9 @@
</ng-template>
</ng-select>
</div>
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
@if (hint) {
<small class="form-text text-muted">{{hint}}</small>
}
</div>
</div>
</div>

View File

@ -13,7 +13,7 @@ import {
catchError,
} from 'rxjs'
import { FILTER_TITLE } from 'src/app/data/filter-rule-type'
import { PaperlessDocument } from 'src/app/data/paperless-document'
import { Document } from 'src/app/data/document'
import { DocumentService } from 'src/app/services/rest/document.service'
import { AbstractInputComponent } from '../abstract-input'
@ -34,9 +34,9 @@ export class DocumentLinkComponent
implements OnInit, OnDestroy
{
documentsInput$ = new Subject<string>()
foundDocuments$: Observable<PaperlessDocument[]>
foundDocuments$: Observable<Document[]>
loading = false
selectedDocuments: PaperlessDocument[] = []
selectedDocuments: Document[] = []
private unsubscribeNotifier: Subject<any> = new Subject()
@ -104,21 +104,18 @@ export class DocumentLinkComponent
)
}
unselect(document: PaperlessDocument): void {
unselect(document: Document): void {
this.selectedDocuments = this.selectedDocuments.filter(
(d) => d.id !== document.id
)
this.onChange(this.selectedDocuments.map((d) => d.id))
}
compareDocuments(
document: PaperlessDocument,
selectedDocument: PaperlessDocument
) {
compareDocuments(document: Document, selectedDocument: Document) {
return document.id === selectedDocument.id
}
trackByFn(item: PaperlessDocument) {
trackByFn(item: Document) {
return item.id
}

View File

@ -2,21 +2,27 @@
<div class="row">
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>
<div class="position-relative" [class.col-md-9]="horizontal">
<div class="input-group" [class.is-invalid]="error">
<input #inputField type="number" class="form-control" [step]="step" [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>
@if (showAdd) {
<button class="btn btn-outline-secondary" type="button" id="button-addon1" (click)="nextAsn()" [disabled]="disabled">+1</button>
}
</div>
<div class="invalid-feedback position-absolute top-100">
{{error}}
</div>
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
@if (hint) {
<small class="form-text text-muted">{{hint}}</small>
}
</div>
</div>
</div>

View File

@ -2,14 +2,18 @@
<label class="form-label" [for]="inputId">{{title}}</label>
<div class="input-group" [class.is-invalid]="error">
<input #inputField [type]="showReveal && textVisible ? 'text' : 'password'" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (focus)="onFocus()" (focusout)="onFocusOut()" (change)="onChange(value)" [disabled]="disabled" [autocomplete]="autocomplete">
<button *ngIf="showReveal" type="button" class="btn btn-outline-secondary" (click)="toggleVisibility()" i18n-title title="Show password" [disabled]="disabled || disableRevealToggle">
@if (showReveal) {
<button type="button" class="btn btn-outline-secondary" (click)="toggleVisibility()" i18n-title title="Show password" [disabled]="disabled || disableRevealToggle">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#eye" />
</svg>
</button>
}
</div>
<div class="invalid-feedback">
{{error}}
</div>
<small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
@if (hint) {
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
}
</div>

View File

@ -1,16 +1,23 @@
<ng-container *ngIf="!accordion">
@if (!accordion) {
<h5 i18n>Permissions</h5>
<ng-container [ngTemplateOutlet]="permissionsForm"></ng-container>
</ng-container>
<ng-container *ngIf="accordion">
<ngb-accordion #acc="ngbAccordion" activeIds="">
<ngb-panel i18n-title title="Edit Permissions">
<ng-template ngbPanelContent>
}
@if (accordion) {
<div ngbAccordion activeIds="">
<div ngbAccordionItem>
<h2 ngbAccordionHeader>
<button ngbAccordionButton i18n>Edit Permissions</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<ng-container [ngTemplateOutlet]="permissionsForm"></ng-container>
</ng-template>
</ngb-panel>
</ngb-accordion>
</ng-container>
</div>
</div>
</div>
</div>
}
<ng-template #permissionsForm>
<div [formGroup]="form">

View File

@ -1,6 +1,6 @@
import { Component, forwardRef, Input, OnInit } from '@angular/core'
import { FormControl, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'
import { PaperlessUser } from 'src/app/data/paperless-user'
import { User } from 'src/app/data/user'
import { AbstractInputComponent } from '../../abstract-input'
export interface PermissionsFormObject {
@ -34,7 +34,7 @@ export class PermissionsFormComponent
implements OnInit
{
@Input()
users: PaperlessUser[]
users: User[]
@Input()
accordion: boolean = false

View File

@ -1,7 +1,7 @@
import { Component, forwardRef, Input, OnInit } from '@angular/core'
import { NG_VALUE_ACCESSOR } from '@angular/forms'
import { first } from 'rxjs/operators'
import { PaperlessGroup } from 'src/app/data/paperless-group'
import { Group } from 'src/app/data/group'
import { GroupService } from 'src/app/services/rest/group.service'
import { AbstractInputComponent } from '../../abstract-input'
@ -17,8 +17,8 @@ import { AbstractInputComponent } from '../../abstract-input'
templateUrl: './permissions-group.component.html',
styleUrls: ['./permissions-group.component.scss'],
})
export class PermissionsGroupComponent extends AbstractInputComponent<PaperlessGroup> {
groups: PaperlessGroup[]
export class PermissionsGroupComponent extends AbstractInputComponent<Group> {
groups: Group[]
constructor(groupService: GroupService) {
super()

View File

@ -1,7 +1,7 @@
import { Component, forwardRef, Input, OnInit } from '@angular/core'
import { NG_VALUE_ACCESSOR } from '@angular/forms'
import { first } from 'rxjs/operators'
import { PaperlessUser } from 'src/app/data/paperless-user'
import { User } from 'src/app/data/user'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
import { AbstractInputComponent } from '../../abstract-input'
@ -18,10 +18,8 @@ import { AbstractInputComponent } from '../../abstract-input'
templateUrl: './permissions-user.component.html',
styleUrls: ['./permissions-user.component.scss'],
})
export class PermissionsUserComponent extends AbstractInputComponent<
PaperlessUser[]
> {
users: PaperlessUser[]
export class PermissionsUserComponent extends AbstractInputComponent<User[]> {
users: User[]
constructor(userService: UserService, settings: SettingsService) {
super()

View File

@ -1,12 +1,16 @@
<div class="mb-3 paperless-input-select" [class.disabled]="disabled">
<div class="row">
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
<label *ngIf="title" class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
@if (title) {
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
}
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>
<div [class.col-md-9]="horizontal">
<div [class.input-group]="allowCreateNew || showFilter" [class.is-invalid]="error">
@ -31,27 +35,35 @@
(clear)="clearLastSearchTerm()"
(blur)="onBlur()">
</ng-select>
<button *ngIf="allowCreateNew" class="btn btn-outline-secondary" type="button" (click)="addItem()" [disabled]="disabled">
@if (allowCreateNew) {
<button class="btn btn-outline-secondary" type="button" (click)="addItem()" [disabled]="disabled">
<svg class="buttonicon" fill="currentColor">
<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" title="{{ filterButtonTitle }}">
}
@if (showFilter) {
<button 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>
</button>
}
</div>
<div class="invalid-feedback">
{{error}}
</div>
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
<small *ngIf="getSuggestions().length > 0">
@if (hint) {
<small class="form-text text-muted">{{hint}}</small>
}
@if (getSuggestions().length > 0) {
<small>
<span i18n>Suggestions:</span>&nbsp;
<ng-container *ngFor="let s of getSuggestions()">
@for (s of getSuggestions(); track s) {
<a (click)="value = s.id; onChange(value)" [routerLink]="[]">{{s.name}}</a>&nbsp;
</ng-container>
}
</small>
}
</div>
</div>
</div>

View File

@ -10,7 +10,7 @@ import {
NG_VALUE_ACCESSOR,
} from '@angular/forms'
import { SelectComponent } from './select.component'
import { PaperlessTag } from 'src/app/data/paperless-tag'
import { Tag } from 'src/app/data/tag'
import {
DEFAULT_MATCHING_ALGORITHM,
MATCH_ALL,
@ -18,7 +18,7 @@ import {
import { NgSelectModule } from '@ng-select/ng-select'
import { RouterTestingModule } from '@angular/router/testing'
const items: PaperlessTag[] = [
const items: Tag[] = [
{
id: 1,
name: 'Tag1',

View File

@ -122,7 +122,7 @@ export class SelectComponent extends AbstractInputComponent<number> {
}
}
addItem(name: string) {
addItem(name: string = null) {
if (name) this.createNew.next(name)
else this.createNew.next(this._lastSearchTerm)
this.clearLastSearchTerm()

View File

@ -21,33 +21,45 @@
<svg width="1.2em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
<pngx-tag *ngIf="item.id && tags" style="background-color: none;" [tag]="getTag(item.id)"></pngx-tag>
@if (item.id && tags) {
<pngx-tag style="background-color: none;" [tag]="getTag(item.id)"></pngx-tag>
}
</span>
</ng-template>
<ng-template ng-option-tmp let-item="item" let-index="index" let-search="searchTerm">
<div class="tag-wrap">
<pngx-tag *ngIf="item.id && tags" class="me-2" [tag]="getTag(item.id)"></pngx-tag>
@if (item.id && tags) {
<pngx-tag class="me-2" [tag]="getTag(item.id)"></pngx-tag>
}
</div>
</ng-template>
</ng-select>
<button *ngIf="allowCreate" class="btn btn-outline-secondary" type="button" (click)="createTag()" [disabled]="disabled">
@if (allowCreate) {
<button class="btn btn-outline-secondary" type="button" (click)="createTag()" [disabled]="disabled">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#plus" />
</svg>
</button>
<button *ngIf="showFilter" class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="hasPrivate || this.value === null" i18n-title title="Filter documents with these Tags">
}
@if (showFilter) {
<button class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="hasPrivate || this.value === null" i18n-title title="Filter documents with these Tags">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#filter" />
</svg>
</button>
}
</div>
<small class="form-text text-muted" *ngIf="hint">{{hint}}</small>
<small *ngIf="getSuggestions().length > 0" class="position-absolute top-100">
@if (hint) {
<small class="form-text text-muted">{{hint}}</small>
}
@if (getSuggestions().length > 0) {
<small class="position-absolute top-100">
<span i18n>Suggestions:</span>&nbsp;
<ng-container *ngFor="let tag of getSuggestions()">
@for (tag of getSuggestions(); track tag) {
<a (click)="addTag(tag.id)" [routerLink]="[]">{{tag?.name}}</a>&nbsp;
</ng-container>
}
</small>
}
</div>
</div>
</div>

View File

@ -1,21 +1,16 @@
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } 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 { Tag } from 'src/app/data/tag'
import {
DEFAULT_MATCHING_ALGORITHM,
MATCH_ALL,
} from 'src/app/data/matching-model'
import { NgSelectComponent, NgSelectModule } from '@ng-select/ng-select'
import { NgSelectModule } from '@ng-select/ng-select'
import { RouterTestingModule } from '@angular/router/testing'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { of } from 'rxjs'
@ -36,7 +31,7 @@ import { PermissionsFormComponent } from '../permissions/permissions-form/permis
import { SelectComponent } from '../select/select.component'
import { SettingsService } from 'src/app/services/settings.service'
const tags: PaperlessTag[] = [
const tags: Tag[] = [
{
id: 1,
name: 'Tag1',

View File

@ -9,7 +9,7 @@ import {
} 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 { Tag } from 'src/app/data/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'
@ -81,13 +81,13 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
horizontal: boolean = false
@Output()
filterDocuments = new EventEmitter<PaperlessTag[]>()
filterDocuments = new EventEmitter<Tag[]>()
@ViewChild('tagSelect') select: NgSelectComponent
value: number[] = []
tags: PaperlessTag[] = []
tags: Tag[] = []
public createTagRef: (name) => void

View File

@ -2,15 +2,19 @@
<div class="row">
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>
<div class="position-relative" [class.col-md-9]="horizontal">
<input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled" [autocomplete]="autocomplete">
<small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
@if (hint) {
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
}
<div class="invalid-feedback position-absolute top-100">
{{error}}
</div>

View File

@ -2,11 +2,13 @@
<div class="row">
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
<button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>
<div [class.col-md-9]="horizontal">
<div class="input-group" [class.is-invalid]="error">
@ -20,7 +22,9 @@
{{error}}
</div>
</div>
<small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
@if (hint) {
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
}
</div>
</div>
</div>

View File

@ -2,7 +2,9 @@
<div class="col-md text-truncate">
<h3 class="text-truncate" style="line-height: 1.4">
{{title}}
<span *ngIf="subTitle" class="h6 mb-0 d-block d-md-inline fw-normal ms-md-3 text-truncate" style="line-height: 1.4">{{subTitle}}</span>
@if (subTitle) {
<span class="h6 mb-0 d-block d-md-inline fw-normal ms-md-3 text-truncate" style="line-height: 1.4">{{subTitle}}</span>
}
</h3>
</div>
<div class="btn-toolbar col col-md-auto">

View File

@ -36,15 +36,6 @@ import { PDFSinglePageViewer } from 'pdfjs-dist/web/pdf_viewer'
PDFJS['verbosity'] = PDFJS.VerbosityLevel.ERRORS
// Yea this is a straight hack
declare global {
interface WeakKeyTypes {
symbol: Object
}
type WeakKey = WeakKeyTypes[keyof WeakKeyTypes]
}
export enum RenderTextMode {
DISABLED,
ENABLED,

View File

@ -5,7 +5,9 @@
</div>
<div class="modal-body">
<p *ngIf="!object && message" class="mb-3" [innerHTML]="message | safeHtml"></p>
@if (!object && message) {
<p class="mb-3" [innerHTML]="message | safeHtml"></p>
}
<form [formGroup]="form">
<pngx-permissions-form [users]="users" formControlName="permissions_form"></pngx-permissions-form>
@ -13,10 +15,10 @@
</div>
<div class="modal-footer">
<ng-container *ngIf="!buttonsEnabled">
@if (!buttonsEnabled) {
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<span class="visually-hidden" i18n>Loading...</span>
</ng-container>
}
<button type="button" class="btn btn-outline-primary" (click)="cancelClicked()" [disabled]="!buttonsEnabled" i18n>Cancel</button>
<button type="button" class="btn btn-primary" (click)="confirmClicked.emit(permissions)" [disabled]="!buttonsEnabled" i18n>Confirm</button>
</div>

View File

@ -2,7 +2,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'
import { FormControl, FormGroup } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
import { PaperlessUser } from 'src/app/data/paperless-user'
import { User } from 'src/app/data/user'
import { UserService } from 'src/app/services/rest/user.service'
@Component({
@ -11,7 +11,7 @@ import { UserService } from 'src/app/services/rest/user.service'
styleUrls: ['./permissions-dialog.component.scss'],
})
export class PermissionsDialogComponent {
users: PaperlessUser[]
users: User[]
private o: ObjectWithPermissions = undefined
constructor(

View File

@ -10,9 +10,11 @@
<div class="list-group list-group-flush">
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.NONE)" [disabled]="disabled">
<div class="selected-icon me-1">
<svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.NONE" fill="currentColor" class="buttonicon-sm">
@if (selectionModel.ownerFilter === OwnerFilterType.NONE) {
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
}
</div>
<div class="me-1">
<small i18n>All</small>
@ -20,9 +22,11 @@
</button>
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.SELF)" [disabled]="disabled">
<div class="selected-icon me-1">
<svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.SELF" fill="currentColor" class="buttonicon-sm">
@if (selectionModel.ownerFilter === OwnerFilterType.SELF) {
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
}
</div>
<div class="me-1">
<small i18n>My documents</small>
@ -30,19 +34,35 @@
</button>
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.NOT_SELF)" [disabled]="disabled">
<div class="selected-icon me-1">
<svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.NOT_SELF" fill="currentColor" class="buttonicon-sm">
@if (selectionModel.ownerFilter === OwnerFilterType.NOT_SELF) {
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
}
</div>
<div class="me-1">
<small i18n>Shared with me</small>
</div>
</button>
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.UNOWNED)" [disabled]="disabled">
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.SHARED_BY_ME)" [disabled]="disabled">
<div class="selected-icon me-1">
<svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.UNOWNED" fill="currentColor" class="buttonicon-sm">
@if (selectionModel.ownerFilter === OwnerFilterType.SHARED_BY_ME) {
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
}
</div>
<div class="me-1">
<small i18n>Shared by me</small>
</div>
</button>
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.UNOWNED)" [disabled]="disabled">
<div class="selected-icon me-1">
@if (selectionModel.ownerFilter === OwnerFilterType.UNOWNED) {
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
}
</div>
<div class="me-1">
<small i18n>Unowned</small>
@ -50,9 +70,11 @@
</button>
<button *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }" class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" [disabled]="disabled">
<div class="selected-icon me-1">
<svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.OTHERS" fill="currentColor" class="buttonicon-sm">
@if (selectionModel.ownerFilter === OwnerFilterType.OTHERS) {
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
}
</div>
<div class="me-1 w-100">
<ng-select
@ -71,12 +93,14 @@
</ng-select>
</div>
</button>
<div *ngIf="selectionModel.ownerFilter === OwnerFilterType.NONE || selectionModel.ownerFilter === OwnerFilterType.NOT_SELF" class="list-group-item list-group-item-action d-flex align-items-center p-2 ps-3 border-bottom-0 border-start-0 border-end-0">
@if (selectionModel.ownerFilter === OwnerFilterType.NONE || selectionModel.ownerFilter === OwnerFilterType.NOT_SELF) {
<div class="list-group-item list-group-item-action d-flex align-items-center p-2 ps-3 border-bottom-0 border-start-0 border-end-0">
<div class="form-check form-switch w-100">
<input type="checkbox" class="form-check-input" id="hideUnowned" [(ngModel)]="this.selectionModel.hideUnowned" (change)="onChange()" [disabled]="disabled">
<label class="form-check-label w-100" for="hideUnowned"><small i18n>Hide unowned</small></label>
</div>
</div>
}
</div>
</div>
</div>

View File

@ -145,6 +145,15 @@ describe('PermissionsFilterDropdownComponent', () => {
userID: null,
})
component.setFilter(OwnerFilterType.SHARED_BY_ME)
expect(ownerFilterSetResult).toEqual({
excludeUsers: [],
hideUnowned: false,
includeUsers: [],
ownerFilter: OwnerFilterType.SHARED_BY_ME,
userID: currentUserID,
})
component.setFilter(OwnerFilterType.UNOWNED)
expect(ownerFilterSetResult).toEqual({
excludeUsers: [],

View File

@ -1,6 +1,6 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'
import { first } from 'rxjs'
import { PaperlessUser } from 'src/app/data/paperless-user'
import { User } from 'src/app/data/user'
import {
PermissionAction,
PermissionType,
@ -32,6 +32,7 @@ export enum OwnerFilterType {
NOT_SELF = 2,
OTHERS = 3,
UNOWNED = 4,
SHARED_BY_ME = 5,
}
@Component({
@ -54,7 +55,7 @@ export class PermissionsFilterDropdownComponent extends ComponentWithPermissions
@Output()
ownerFilterSet = new EventEmitter<PermissionsSelectionModel>()
users: PaperlessUser[]
users: User[]
hideUnowned: boolean
@ -108,6 +109,13 @@ export class PermissionsFilterDropdownComponent extends ComponentWithPermissions
this.selectionModel.includeUsers = []
this.selectionModel.excludeUsers = []
this.selectionModel.hideUnowned = false
} else if (
this.selectionModel.ownerFilter === OwnerFilterType.SHARED_BY_ME
) {
this.selectionModel.userID = this.settingsService.currentUser.id
this.selectionModel.includeUsers = []
this.selectionModel.excludeUsers = []
this.selectionModel.hideUnowned = false
} else if (this.selectionModel.ownerFilter === OwnerFilterType.UNOWNED) {
this.selectionModel.userID = null
this.selectionModel.includeUsers = []

View File

@ -9,19 +9,23 @@
<div class="col" i18n>Delete</div>
<div class="col" i18n>View</div>
</li>
<li class="list-group-item d-flex" *ngFor="let type of PermissionType | keyvalue" [formGroupName]="type.key">
@for (type of PermissionType | keyvalue; track type) {
<li class="list-group-item d-flex" [formGroupName]="type.key">
<div class="col-3">{{type.key}}:</div>
<div class="col form-check form-check-inline form-switch" [ngbPopover]="inheritedWarning" [disablePopover]="!isInherited(type.key)" placement="left" triggers="mouseenter:mouseleave">
<input type="checkbox" class="form-check-input" id="{{type.key}}_all" (change)="toggleAll($event, type.key)" [checked]="typesWithAllActions.has(type.key) || isInherited(type.key)" [attr.disabled]="disabled || isInherited(type.key) ? true : null">
<label class="form-check-label visually-hidden" for="{{type.key}}_all" i18n>All</label>
</div>
<div *ngFor="let action of PermissionAction | keyvalue" class="col form-check form-check-inline" [ngbPopover]="inheritedWarning" [disablePopover]="!isInherited(type.key, action.key)" placement="left" triggers="mouseenter:mouseleave">
@for (action of PermissionAction | keyvalue; track action) {
<div class="col form-check form-check-inline" [ngbPopover]="inheritedWarning" [disablePopover]="!isInherited(type.key, action.key)" placement="left" triggers="mouseenter:mouseleave">
<input type="checkbox" class="form-check-input" id="{{type.key}}_{{action.key}}" formControlName="{{action.key}}">
<label class="form-check-label visually-hidden" for="{{type.key}}_{{action.key}}" i18n>{{action.key}}</label>
</div>
}
</li>
<div *ngIf="error" class="invalid-feedback d-block">{{error}}</div>
}
@if (error) {
<div class="invalid-feedback d-block">{{error}}</div>
}
</ul>
</form>

View File

@ -1,22 +1,28 @@
<div class="preview-popup-container">
<div *ngIf="error; else noError" class="w-100 h-100 position-relative">
@if (error) {
<div class="w-100 h-100 position-relative">
<p class="fst-italic position-absolute top-50 start-50 translate-middle" i18n>Error loading preview</p>
</div>
<ng-template #noError>
<object *ngIf="renderAsObject; else pngxViewer" [data]="previewURL | safeUrl" width="100%" class="bg-light" [class.p-2]="!isPdf"></object>
<ng-template #pngxViewer>
<div *ngIf="requiresPassword" class="w-100 h-100 position-relative">
} @else {
@if (renderAsObject) {
<object [data]="previewURL | safeUrl" width="100%" class="bg-light" [class.p-2]="!isPdf"></object>
} @else {
@if (requiresPassword) {
<div class="w-100 h-100 position-relative">
<svg width="2em" height="2em" fill="currentColor" class="position-absolute top-50 start-50 translate-middle">
<use xlink:href="assets/bootstrap-icons.svg#file-earmark-lock"/>
</svg>
</div>
<pngx-pdf-viewer *ngIf="!requiresPassword"
}
@if (!requiresPassword) {
<pngx-pdf-viewer
[src]="previewURL"
[original-size]="false"
[show-borders]="true"
[show-all]="true"
(error)="onError($event)">
</pngx-pdf-viewer>
</ng-template>
</ng-template>
}
}
}
</div>

View File

@ -5,7 +5,7 @@ import { PdfViewerComponent } from '../pdf-viewer/pdf-viewer.component'
import { By } from '@angular/platform-browser'
import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
import { SettingsService } from 'src/app/services/settings.service'
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { DocumentService } from 'src/app/services/rest/document.service'

View File

@ -1,6 +1,6 @@
import { Component, Input } from '@angular/core'
import { PaperlessDocument } from 'src/app/data/paperless-document'
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
import { Document } from 'src/app/data/document'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { DocumentService } from 'src/app/services/rest/document.service'
import { SettingsService } from 'src/app/services/settings.service'
@ -11,7 +11,7 @@ import { SettingsService } from 'src/app/services/settings.service'
})
export class PreviewPopupComponent {
@Input()
document: PaperlessDocument
document: Document
error = false

View File

@ -34,8 +34,12 @@
<input type="text" class="form-control" formControlName="auth_token" readonly>
<button type="button" class="btn btn-outline-secondary" (click)="copyAuthToken()" i18n-title title="Copy">
<svg class="buttonicon-sm" fill="currentColor">
<use *ngIf="!copied" xlink:href="assets/bootstrap-icons.svg#clipboard-fill" />
<use *ngIf="copied" xlink:href="assets/bootstrap-icons.svg#clipboard-check-fill" />
@if (!copied) {
<use xlink:href="assets/bootstrap-icons.svg#clipboard-fill" />
}
@if (copied) {
<use xlink:href="assets/bootstrap-icons.svg#clipboard-check-fill" />
}
</svg><span class="visually-hidden" i18n>Copy</span>
</button>
<button type="button" class="btn btn-outline-secondary" (click)="generateAuthToken()" i18n-title title="Regenerate auth token">

View File

@ -7,26 +7,37 @@
</button>
<div ngbDropdownMenu aria-labelledby="shareLinksDropdown" class="shadow share-links-dropdown">
<ul class="list-group list-group-flush">
<li *ngIf="!shareLinks || shareLinks.length === 0" class="list-group-item fst-italic small text-center text-secondary" i18n>
@if (!shareLinks || shareLinks.length === 0) {
<li class="list-group-item fst-italic small text-center text-secondary" i18n>
No existing links
</li>
<li class="list-group-item" *ngFor="let link of shareLinks">
}
@for (link of shareLinks; track link) {
<li class="list-group-item">
<div class="input-group input-group-sm w-100">
<input type="text" class="form-control" aria-label="Share link" [value]="getShareUrl(link)" readonly>
<span *ngIf="link.expiration" class="input-group-text">
@if (link.expiration) {
<span class="input-group-text">
{{ getDaysRemaining(link) }}
</span>
}
<button type="button" class="btn btn-sm btn-outline-primary" (click)="copy(link)">
<svg class="buttonicon" fill="currentColor">
<use *ngIf="copied !== link.id" xlink:href="assets/bootstrap-icons.svg#clipboard-fill" />
<use *ngIf="copied === link.id" xlink:href="assets/bootstrap-icons.svg#clipboard-check-fill" />
@if (copied !== link.id) {
<use xlink:href="assets/bootstrap-icons.svg#clipboard-fill" />
}
@if (copied === link.id) {
<use xlink:href="assets/bootstrap-icons.svg#clipboard-check-fill" />
}
</svg><span class="visually-hidden" i18n>Copy</span>
</button>
<button *ngIf="canShare(link)" type="button" class="btn btn-sm btn-outline-primary" (click)="share(link)">
@if (canShare(link)) {
<button type="button" class="btn btn-sm btn-outline-primary" (click)="share(link)">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#box-arrow-up" />
</svg><span class="visually-hidden" i18n>Share</span>
</button>
}
<button type="button" class="btn btn-sm btn-outline-danger" (click)="delete(link)">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" />
@ -35,6 +46,7 @@
</div>
<span class="badge copied-badge bg-primary small fade ms-4 position-absolute top-50 translate-middle-y pe-none z-3" [class.show]="copied === link.id" i18n>Copied!</span>
</li>
}
<li class="list-group-item pt-3 pb-2">
<div class="input-group input-group-sm w-100">
<div class="form-check form-switch ms-auto small">
@ -45,13 +57,19 @@
<div class="input-group input-group-sm w-100 mt-2">
<label class="input-group-text" for="addLink"><ng-container i18n>Expires</ng-container>:</label>
<select class="form-select form-select-sm" [(ngModel)]="expirationDays">
<option *ngFor="let option of EXPIRATION_OPTIONS" [ngValue]="option.value">{{ option.label }}</option>
@for (option of EXPIRATION_OPTIONS; track option) {
<option [ngValue]="option.value">{{ option.label }}</option>
}
</select>
<button class="btn btn-sm btn-outline-primary ms-auto" type="button" (click)="createLink()" [disabled]="loading">
<div *ngIf="loading" class="spinner-border spinner-border-sm me-2" role="status"></div>
<svg *ngIf="!loading" class="buttonicon me-1" fill="currentColor">
@if (loading) {
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
}
@if (!loading) {
<svg class="buttonicon me-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#plus" />
</svg>
}
<ng-container i18n>Create</ng-container>
</button>
</div>

View File

@ -10,10 +10,7 @@ import {
} from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { of, throwError } from 'rxjs'
import {
PaperlessFileVersion,
PaperlessShareLink,
} from 'src/app/data/paperless-share-link'
import { FileVersion, 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'
@ -60,7 +57,7 @@ describe('ShareLinksDropdownComponent', () => {
slug: '1234slug',
created: now.toISOString(),
document: 99,
file_version: PaperlessFileVersion.Archive,
file_version: FileVersion.Archive,
expiration: expiration7days.toISOString(),
},
{
@ -68,7 +65,7 @@ describe('ShareLinksDropdownComponent', () => {
slug: '1234slug',
created: now.toISOString(),
document: 99,
file_version: PaperlessFileVersion.Original,
file_version: FileVersion.Original,
expiration: null,
},
])
@ -152,7 +149,7 @@ describe('ShareLinksDropdownComponent', () => {
deleteSpy.mockReturnValue(of(true))
const refreshSpy = jest.spyOn(component, 'refresh')
component.delete({ id: 12 } as PaperlessShareLink)
component.delete({ id: 12 } as ShareLink)
fixture.detectChanges()
expect(deleteSpy).toHaveBeenCalledWith({ id: 12 })
expect(refreshSpy).toHaveBeenCalled()
@ -178,18 +175,18 @@ describe('ShareLinksDropdownComponent', () => {
expect(
component.getDaysRemaining({
expiration: expiration7days.toISOString(),
} as PaperlessShareLink)
} as ShareLink)
).toEqual('7 days')
expect(
component.getDaysRemaining({
expiration: expiration1day.toISOString(),
} as PaperlessShareLink)
} as ShareLink)
).toEqual('1 day')
})
// coverage
it('should support share', () => {
const link = { slug: '12345slug' } as PaperlessShareLink
const link = { slug: '12345slug' } as ShareLink
if (!('share' in navigator))
Object.defineProperty(navigator, 'share', { value: (obj: any) => {} })
// const navigatorSpy = jest.spyOn(navigator, 'share')

View File

@ -1,9 +1,6 @@
import { Component, Input, OnInit } from '@angular/core'
import { first } from 'rxjs'
import {
PaperlessShareLink,
PaperlessFileVersion,
} from 'src/app/data/paperless-share-link'
import { ShareLink, FileVersion } 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'
@ -41,7 +38,7 @@ export class ShareLinksDropdownComponent implements OnInit {
@Input()
hasArchiveVersion: boolean = true
shareLinks: PaperlessShareLink[]
shareLinks: ShareLink[]
loading: boolean = false
@ -83,21 +80,21 @@ export class ShareLinksDropdownComponent implements OnInit {
})
}
getShareUrl(link: PaperlessShareLink): string {
getShareUrl(link: ShareLink): string {
const apiURL = new URL(environment.apiBaseUrl)
return `${apiURL.origin}${apiURL.pathname.replace(/\/api\/$/, '/share/')}${
link.slug
}`
}
getDaysRemaining(link: PaperlessShareLink): string {
getDaysRemaining(link: ShareLink): string {
const days: number = Math.round(
(Date.parse(link.expiration) - Date.now()) / (1000 * 60 * 60 * 24)
)
return days === 1 ? $localize`1 day` : $localize`${days} days`
}
copy(link: PaperlessShareLink) {
copy(link: ShareLink) {
const success = this.clipboard.copy(this.getShareUrl(link))
if (success) {
this.copied = link.id
@ -107,17 +104,17 @@ export class ShareLinksDropdownComponent implements OnInit {
}
}
canShare(link: PaperlessShareLink): boolean {
canShare(link: ShareLink): boolean {
return (
navigator?.canShare && navigator.canShare({ url: this.getShareUrl(link) })
)
}
share(link: PaperlessShareLink) {
share(link: ShareLink) {
navigator.share({ url: this.getShareUrl(link) })
}
delete(link: PaperlessShareLink) {
delete(link: ShareLink) {
this.shareLinkService.delete(link).subscribe({
next: () => {
this.refresh()
@ -138,9 +135,7 @@ export class ShareLinksDropdownComponent implements OnInit {
this.shareLinkService
.createLinkForDocument(
this._documentId,
this.useArchiveVersion
? PaperlessFileVersion.Archive
: PaperlessFileVersion.Original,
this.useArchiveVersion ? FileVersion.Archive : FileVersion.Original,
expiration
)
.subscribe({

View File

@ -1,9 +1,15 @@
<ng-container *ngIf="tag !== undefined; else privateTag" >
<span *ngIf="!clickable" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</span>
<a [title]="linkTitle" *ngIf="clickable" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</a>
</ng-container>
<ng-template #privateTag>
<span *ngIf="!clickable" class="badge private" i18n>Private</span>
<a [title]="linkTitle" *ngIf="clickable" class="badge private" i18n>Private</a>
</ng-template>
@if (tag !== undefined) {
@if (!clickable) {
<span class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</span>
}
@if (clickable) {
<a [title]="linkTitle" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</a>
}
} @else {
@if (!clickable) {
<span class="badge private" i18n>Private</span>
}
@if (clickable) {
<a [title]="linkTitle" class="badge private" i18n>Private</a>
}
}

View File

@ -1,9 +1,9 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { TagComponent } from './tag.component'
import { PaperlessTag } from 'src/app/data/paperless-tag'
import { Tag } from 'src/app/data/tag'
import { By } from '@angular/platform-browser'
const tag: PaperlessTag = {
const tag: Tag = {
id: 1,
color: '#ff0000',
name: 'Tag1',

View File

@ -1,5 +1,5 @@
import { Component, Input } from '@angular/core'
import { PaperlessTag } from 'src/app/data/paperless-tag'
import { Tag } from 'src/app/data/tag'
@Component({
selector: 'pngx-tag',
@ -10,7 +10,7 @@ export class TagComponent {
constructor() {}
@Input()
tag: PaperlessTag
tag: Tag
@Input()
linkTitle: string = ''

View File

@ -1,5 +1,5 @@
@for (toast of toasts; track toast) {
<ngb-toast
*ngFor="let toast of toasts"
[autohide]="true" [delay]="toast.delay"
[class]="toast.classname"
[class.mb-2]="true"
@ -9,14 +9,20 @@
<span class="visually-hidden">{{ toast.delayRemaining / 1000 | number: '1.0-0' }} seconds</span>
<div class="d-flex align-items-top">
<svg class="sidebaricon-sm mt-1 me-2 flex-shrink-0" fill="currentColor">
<use *ngIf="!toast.error" xlink:href="assets/bootstrap-icons.svg#info-circle"/>
<use *ngIf="toast.error" xlink:href="assets/bootstrap-icons.svg#exclamation-triangle"/>
@if (!toast.error) {
<use xlink:href="assets/bootstrap-icons.svg#info-circle"/>
}
@if (toast.error) {
<use xlink:href="assets/bootstrap-icons.svg#exclamation-triangle"/>
}
</svg>
<div>
<p class="mb-0">{{toast.content}}</p>
<details *ngIf="toast.error">
@if (toast.error) {
<details>
<div class="mt-2">
<dl class="row mb-0" *ngIf="isDetailedError(toast.error)">
@if (isDetailedError(toast.error)) {
<dl class="row mb-0">
<dt class="col-sm-3 fw-normal text-end">URL</dt>
<dd class="col-sm-9">{{ toast.error.url }}</dd>
<dt class="col-sm-3 fw-normal text-end" i18n>Status</dt>
@ -24,20 +30,29 @@
<dt class="col-sm-3 fw-normal text-end" i18n>Error</dt>
<dd class="col-sm-9">{{ getErrorText(toast.error) }}</dd>
</dl>
}
<div class="row">
<div class="col offset-sm-3">
<button class="btn btn-sm btn-outline-dark" (click)="copyError(toast.error)">
<svg class="sidebaricon" fill="currentColor">
<use *ngIf="!copied" xlink:href="assets/bootstrap-icons.svg#clipboard" />
<use *ngIf="copied" xlink:href="assets/bootstrap-icons.svg#clipboard-check" />
@if (!copied) {
<use xlink:href="assets/bootstrap-icons.svg#clipboard" />
}
@if (copied) {
<use xlink:href="assets/bootstrap-icons.svg#clipboard-check" />
}
</svg>&nbsp;<ng-container i18n>Copy Raw Error</ng-container>
</button>
</div>
</div>
</div>
</details>
<p class="mb-0 mt-2" *ngIf="toast.action"><button class="btn btn-sm btn-outline-secondary" (click)="toastService.closeToast(toast); toast.action()">{{toast.actionName}}</button></p>
}
@if (toast.action) {
<p class="mb-0 mt-2"><button class="btn btn-sm btn-outline-secondary" (click)="toastService.closeToast(toast); toast.action()">{{toast.actionName}}</button></p>
}
</div>
<button type="button" class="btn-close ms-auto flex-shrink-0" data-bs-dismiss="toast" aria-label="Close" (click)="toastService.closeToast(toast);"></button>
</div>
</ngb-toast>
}

View File

@ -10,7 +10,7 @@ import { Clipboard } from '@angular/cdk/clipboard'
})
export class ToastsComponent implements OnInit, OnDestroy {
constructor(
private toastService: ToastService,
public toastService: ToastService,
private clipboard: Clipboard
) {}

View File

@ -9,17 +9,22 @@
[cdkDropListDisabled]="settingsService.globalDropzoneActive"
(cdkDropListDropped)="onDrop($event)"
>
<div *ngIf="savedViewService.loading" class="col">
@if (savedViewService.loading) {
<div class="col">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container>
</div>
}
<div *ngIf="settingsService.offerTour()" class="col">
@if (settingsService.offerTour()) {
<div class="col">
<pngx-welcome-widget (dismiss)="completeTour()"></pngx-welcome-widget>
</div>
}
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
<div *ngFor="let v of dashboardViews" class="col">
@for (v of dashboardViews; track v) {
<div class="col">
<pngx-saved-view-widget
[savedView]="v"
(cdkDragStarted)="onDragStart($event)"
@ -27,6 +32,7 @@
>
</pngx-saved-view-widget>
</div>
}
</ng-container>
</div>
</div>

View File

@ -18,9 +18,9 @@ import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap'
import { LogoComponent } from '../common/logo/logo.component'
import { of, throwError } from 'rxjs'
import { ToastService } from 'src/app/services/toast.service'
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop'
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
import { SavedView } from 'src/app/data/saved-view'
const saved_views = [
{
@ -167,7 +167,7 @@ describe('DashboardComponent', () => {
const toastSpy = jest.spyOn(toastService, 'showInfo')
jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true))
component.onDrop({ previousIndex: 0, currentIndex: 1 } as CdkDragDrop<
PaperlessSavedView[]
SavedView[]
>)
expect(settingsSpy).toHaveBeenCalledWith([
saved_views[2],
@ -190,7 +190,7 @@ describe('DashboardComponent', () => {
.spyOn(settingsService, 'storeSettings')
.mockReturnValue(throwError(() => new Error('unable to save')))
component.onDrop({ previousIndex: 0, currentIndex: 2 } as CdkDragDrop<
PaperlessSavedView[]
SavedView[]
>)
expect(toastSpy).toHaveBeenCalled()
})

View File

@ -3,9 +3,9 @@ import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
import { SavedView } from 'src/app/data/saved-view'
import { ToastService } from 'src/app/services/toast.service'
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import {
CdkDragDrop,
CdkDragEnd,
@ -19,7 +19,7 @@ import {
styleUrls: ['./dashboard.component.scss'],
})
export class DashboardComponent extends ComponentWithPermissions {
public dashboardViews: PaperlessSavedView[] = []
public dashboardViews: SavedView[] = []
constructor(
public settingsService: SettingsService,
public savedViewService: SavedViewService,
@ -57,7 +57,7 @@ export class DashboardComponent extends ComponentWithPermissions {
this.settingsService.globalDropzoneEnabled = true
}
onDrop(event: CdkDragDrop<PaperlessSavedView[]>) {
onDrop(event: CdkDragDrop<SavedView[]>) {
moveItemInArray(
this.dashboardViews,
event.previousIndex,

View File

@ -5,9 +5,12 @@
[draggable]="savedView"
>
<a *ngIf="documents.length" class="btn-link text-decoration-none" header-buttons [routerLink]="[]" (click)="showAll()" i18n>Show all</a>
@if (documents.length) {
<a class="btn-link text-decoration-none" header-buttons [routerLink]="[]" (click)="showAll()" i18n>Show all</a>
}
<table *ngIf="documents.length; else empty" content class="table table-hover mb-0 align-middle">
@if (documents.length) {
<table content class="table table-hover mb-0 align-middle">
<thead>
<tr>
<th scope="col" i18n>Created</th>
@ -17,16 +20,21 @@
</tr>
</thead>
<tbody>
<tr *ngFor="let doc of documents" (mouseleave)="maybeClosePopover()">
@for (doc of documents; track doc) {
<tr (mouseleave)="maybeClosePopover()">
<td class="py-2 py-md-3"><a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.created_date | customDate}}</a></td>
<td class="py-2 py-md-3">
<a routerLink="/documents/{{doc.id}}" title="Edit" i18n-title class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.title | documentTitle}}</a>
</td>
<td class="py-2 py-md-3 d-none d-md-table-cell">
<pngx-tag [tag]="t" *ngFor="let t of doc.tags$ | async" class="ms-1" (click)="clickTag(t, $event)"></pngx-tag>
@for (t of doc.tags$ | async; track t) {
<pngx-tag [tag]="t" class="ms-1" (click)="clickTag(t, $event)"></pngx-tag>
}
</td>
<td class="position-relative py-2 py-md-3 d-none d-md-table-cell">
<a *ngIf="doc.correspondent !== null" class="btn-link text-dark text-decoration-none py-2 py-md-3" routerLink="/documents" [queryParams]="getCorrespondentQueryParams(doc.correspondent)">{{(doc.correspondent$ | async)?.name}}</a>
@if (doc.correspondent !== null) {
<a class="btn-link text-dark text-decoration-none py-2 py-md-3" routerLink="/documents" [queryParams]="getCorrespondentQueryParams(doc.correspondent)">{{(doc.correspondent$ | async)?.name}}</a>
}
<div class="btn-group position-absolute top-50 end-0 translate-middle-y">
<a [href]="getPreviewUrl(doc)" title="View Preview" i18n-title target="_blank" class="btn px-4 btn-dark border-dark-subtle"
[ngbPopover]="previewContent" [popoverTitle]="doc.title | documentTitle"
@ -46,11 +54,12 @@
</div>
</td>
</tr>
}
</tbody>
</table>
<ng-template #empty>
} @else {
<p i18n class="text-center text-muted mb-0 fst-italic">No documents</p>
</ng-template>
}
</pngx-widget-frame>

Some files were not shown because too many files have changed in this diff Show More