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. 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 ## Type of change
@ -17,10 +21,11 @@ What type of change does your PR introduce to Paperless-ngx?
NOTE: Please check only one box! NOTE: Please check only one box!
--> -->
- [ ] Bug fix (non-breaking change which fixes an issue) - [ ] Bug fix: non-breaking change which fixes an issue.
- [ ] New feature (non-breaking change which adds functionality) - [ ] 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) - [ ] Breaking change: fix or feature that would cause existing functionality to not work as expected.
- [ ] Other (please explain): - [ ] Documentation only.
- [ ] Other. Please explain:
## Checklist: ## Checklist:

View File

@ -16,7 +16,7 @@ on:
env: env:
# This is the version of pipenv all the steps will use # This is the version of pipenv all the steps will use
# If changing this, change Dockerfile # 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 # This is the default version of Python to use in most steps which aren't specific
DEFAULT_PYTHON_VERSION: "3.10" 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-ui/angular.json (under the _projects/paperless-ui/i18n/locales_ JSON key)
- src/paperless/settings.py (in the _LANGUAGES_ array) - 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_) - 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. 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 WORKDIR /src/src-ui
RUN set -eux \ RUN set -eux \
&& npm update npm -g \ && npm update npm -g \
&& npm ci --omit=optional && npm ci
RUN set -eux \ RUN set -eux \
&& ./node_modules/.bin/ng build --configuration production && ./node_modules/.bin/ng build --configuration production
@ -29,7 +29,7 @@ COPY Pipfile* ./
RUN set -eux \ RUN set -eux \
&& echo "Installing pipenv" \ && 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" \ && echo "Generating requirement.txt" \
&& pipenv requirements > requirements.txt && pipenv requirements > requirements.txt

View File

@ -57,7 +57,7 @@ zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"}
[dev-packages] [dev-packages]
# Linting # Linting
black = "*" black = "==23.11.0"
pre-commit = "*" pre-commit = "*"
ruff = "*" ruff = "*"
# Testing # 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() { search_index() {
local -r index_version=7 local -r index_version=8
local -r index_version_file=${DATA_DIR}/.index_version local -r index_version_file=${DATA_DIR}/.index_version
if [[ (! -f "${index_version_file}") || $(<"${index_version_file}") != "$index_version" ]]; then 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`: `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 `dateInputFormat` is a special string that defines the behavior of
the date input fields and absolutely needs to contain "dd", "mm" the date input fields and absolutely needs to contain "dd", "mm"
and "yyyy". and "yyyy".
```
3. Import and register the Angular data for this locale in 3. Import and register the Angular data for this locale in
`src-ui/src/app/app.module.ts`: `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 - `Integer`: integer number e.g. 12
- `Number`: float number e.g. 12.3456 - `Number`: float number e.g. 12.3456
- `Monetary`: float number with exactly two decimals, e.g. 12.30 - `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 ## Share Links

View File

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

View File

@ -1657,7 +1657,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2941198503117307737" datatype="html"> <trans-unit id="2941198503117307737" datatype="html">
@ -1721,7 +1721,7 @@
</context-group> </context-group>
<context-group purpose="location"> <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="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>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/consumption-templates/consumption-templates.component.html</context> <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 context-type="linenumber">38</context>
</context-group> </context-group>
</trans-unit> </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"> <trans-unit id="5151074932731293042" datatype="html">
<source>Unowned</source> <source>Unowned</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8999708063434507268" datatype="html"> <trans-unit id="8999708063434507268" datatype="html">
<source>Hide unowned</source> <source>Hide unowned</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8650499415827640724" datatype="html"> <trans-unit id="8650499415827640724" datatype="html">
@ -4012,7 +4019,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <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>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/services/rest/document.service.ts</context> <context context-type="sourcefile">src/app/services/rest/document.service.ts</context>
@ -4073,7 +4080,7 @@
</context-group> </context-group>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="872092479747931526" datatype="html"> <trans-unit id="872092479747931526" datatype="html">
@ -5007,11 +5014,26 @@
<context context-type="linenumber">58,59</context> <context context-type="linenumber">58,59</context>
</context-group> </context-group>
</trans-unit> </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"> <trans-unit id="2332107018974972998" datatype="html">
<source>Score:</source> <source>Score:</source>
<context-group purpose="location"> <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="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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3661756380991326939" datatype="html"> <trans-unit id="3661756380991326939" datatype="html">
@ -5138,7 +5160,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <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>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/services/rest/document.service.ts</context> <context context-type="sourcefile">src/app/services/rest/document.service.ts</context>
@ -5254,14 +5276,14 @@
)?.name"/></source> )?.name"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8170755470576301659" datatype="html"> <trans-unit id="8170755470576301659" datatype="html">
<source>Without correspondent</source> <source>Without correspondent</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="317796810569008208" datatype="html"> <trans-unit id="317796810569008208" datatype="html">
@ -5270,14 +5292,14 @@
)?.name"/></source> )?.name"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4362173610367509215" datatype="html"> <trans-unit id="4362173610367509215" datatype="html">
<source>Without document type</source> <source>Without document type</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="232202047340644471" datatype="html"> <trans-unit id="232202047340644471" datatype="html">
@ -5286,14 +5308,14 @@
)?.name"/></source> )?.name"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1562820715074533164" datatype="html"> <trans-unit id="1562820715074533164" datatype="html">
<source>Without storage path</source> <source>Without storage path</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8180755793012580465" datatype="html"> <trans-unit id="8180755793012580465" datatype="html">
@ -5301,112 +5323,112 @@
?.name"/></source> ?.name"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6494566478302448576" datatype="html"> <trans-unit id="6494566478302448576" datatype="html">
<source>Without any tag</source> <source>Without any tag</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6523384805359286307" datatype="html"> <trans-unit id="6523384805359286307" datatype="html">
<source>Title: <x id="PH" equiv-text="rule.value"/></source> <source>Title: <x id="PH" equiv-text="rule.value"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1872523635812236432" datatype="html"> <trans-unit id="1872523635812236432" datatype="html">
<source>ASN: <x id="PH" equiv-text="rule.value"/></source> <source>ASN: <x id="PH" equiv-text="rule.value"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="102674688969746976" datatype="html"> <trans-unit id="102674688969746976" datatype="html">
<source>Owner: <x id="PH" equiv-text="rule.value"/></source> <source>Owner: <x id="PH" equiv-text="rule.value"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3550877650686009106" datatype="html"> <trans-unit id="3550877650686009106" datatype="html">
<source>Owner not in: <x id="PH" equiv-text="rule.value"/></source> <source>Owner not in: <x id="PH" equiv-text="rule.value"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1082034558646673343" datatype="html"> <trans-unit id="1082034558646673343" datatype="html">
<source>Without an owner</source> <source>Without an owner</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3100631071441658964" datatype="html"> <trans-unit id="3100631071441658964" datatype="html">
<source>Title &amp; content</source> <source>Title &amp; content</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="9149498548977462220" datatype="html"> <trans-unit id="9149498548977462220" datatype="html">
<source>Custom fields</source> <source>Custom fields</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1010505078885609376" datatype="html"> <trans-unit id="1010505078885609376" datatype="html">
<source>Advanced search</source> <source>Advanced search</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2649431021108393503" datatype="html"> <trans-unit id="2649431021108393503" datatype="html">
<source>More like</source> <source>More like</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3697582909018473071" datatype="html"> <trans-unit id="3697582909018473071" datatype="html">
<source>equals</source> <source>equals</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5325481293405718739" datatype="html"> <trans-unit id="5325481293405718739" datatype="html">
<source>is empty</source> <source>is empty</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6166785695326182482" datatype="html"> <trans-unit id="6166785695326182482" datatype="html">
<source>is not empty</source> <source>is not empty</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4686622206659266699" datatype="html"> <trans-unit id="4686622206659266699" datatype="html">
<source>greater than</source> <source>greater than</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8014012170270529279" datatype="html"> <trans-unit id="8014012170270529279" datatype="html">
<source>less than</source> <source>less than</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7210076240260527720" datatype="html"> <trans-unit id="7210076240260527720" datatype="html">
@ -6297,13 +6319,6 @@
<context context-type="linenumber">11</context> <context context-type="linenumber">11</context>
</context-group> </context-group>
</trans-unit> </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"> <trans-unit id="2807800733729323332" datatype="html">
<source>Yes</source> <source>Yes</source>
<context-group purpose="location"> <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, "private": true,
"dependencies": { "dependencies": {
"@angular/cdk": "^16.2.11", "@angular/cdk": "^17.0.4",
"@angular/common": "~16.2.11", "@angular/common": "~17.0.7",
"@angular/compiler": "~16.2.11", "@angular/compiler": "~17.0.7",
"@angular/core": "~16.2.11", "@angular/core": "~17.0.7",
"@angular/forms": "~16.2.11", "@angular/forms": "~17.0.7",
"@angular/localize": "~16.2.11", "@angular/localize": "~17.0.7",
"@angular/platform-browser": "~16.2.11", "@angular/platform-browser": "~17.0.7",
"@angular/platform-browser-dynamic": "~16.2.11", "@angular/platform-browser-dynamic": "~17.0.7",
"@angular/router": "~16.2.11", "@angular/router": "~17.0.7",
"@ng-bootstrap/ng-bootstrap": "^15.1.2", "@ng-bootstrap/ng-bootstrap": "^16.0.0",
"@ng-select/ng-select": "^11.2.0", "@ng-select/ng-select": "^12.0.4",
"@ngneat/dirty-check-forms": "^3.0.3", "@ngneat/dirty-check-forms": "^3.0.3",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.2", "bootstrap": "^5.3.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"mime-names": "^1.0.0", "mime-names": "^1.0.0",
"ngx-color": "^9.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-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", "pdfjs-dist": "^3.11.174",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"tslib": "^2.6.2", "tslib": "^2.6.2",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"zone.js": "^0.13.3" "zone.js": "^0.14.2"
}, },
"devDependencies": { "devDependencies": {
"@angular-builders/jest": "16.0.1", "@angular-builders/jest": "17.0.0",
"@angular-devkit/build-angular": "~16.2.9", "@angular-devkit/build-angular": "~17.0.7",
"@angular-eslint/builder": "16.2.0", "@angular-eslint/builder": "17.1.1",
"@angular-eslint/eslint-plugin": "16.2.0", "@angular-eslint/eslint-plugin": "17.1.1",
"@angular-eslint/eslint-plugin-template": "16.2.0", "@angular-eslint/eslint-plugin-template": "17.1.1",
"@angular-eslint/schematics": "16.2.0", "@angular-eslint/schematics": "17.1.1",
"@angular-eslint/template-parser": "16.2.0", "@angular-eslint/template-parser": "17.1.1",
"@angular/cli": "~16.2.9", "@angular/cli": "~17.0.7",
"@angular/compiler-cli": "~16.2.3", "@angular/compiler-cli": "~17.0.7",
"@playwright/test": "^1.40.1", "@playwright/test": "^1.40.1",
"@types/jest": "^29.5.10", "@types/jest": "^29.5.10",
"@types/node": "^20.10.2", "@types/node": "^20.10.2",
"@typescript-eslint/eslint-plugin": "^6.13.1", "@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.13.1", "@typescript-eslint/parser": "^6.10.0",
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"eslint": "^8.55.0", "eslint": "^8.53.0",
"jest": "29.7.0", "jest": "29.7.0",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"jest-preset-angular": "^13.1.4", "jest-preset-angular": "^13.1.4",
"jest-websocket-mock": "^2.5.0", "jest-websocket-mock": "^2.5.0",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"ts-node": "~10.9.1", "ts-node": "~10.9.1",
"typescript": "^5.1.6", "typescript": "^5.2.2",
"wait-on": "^7.2.0" "wait-on": "^7.2.0"
} }
} }

View File

@ -19,12 +19,16 @@
</button> </button>
</div> </div>
<div class="btn-group btn-group-sm align-self-end" role="group" aria-label="Previous / Next"> <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 }} « {{ step?.prevBtnTitle }}
</button> </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 }} » {{ step?.nextBtnTitle }} »
</button> </button>
}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,5 +1,5 @@
import { SettingsService } from './services/settings.service' 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 { Component, OnDestroy, OnInit, Renderer2 } from '@angular/core'
import { Router } from '@angular/router' import { Router } from '@angular/router'
import { Subscription, first } from 'rxjs' import { Subscription, first } from 'rxjs'

View File

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

View File

@ -24,10 +24,16 @@
<div class="col"> <div class="col">
<select class="form-select" formControlName="displayLanguage"> <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> </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>
</div> </div>
@ -39,7 +45,11 @@
<div class="col"> <div class="col">
<select class="form-select" formControlName="dateLocale"> <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> </select>
</div> </div>
@ -281,12 +291,12 @@
<h4 i18n>Views</h4> <h4 i18n>Views</h4>
<div formGroupName="savedViews"> <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"> <div class="mb-3 col">
<label class="form-label" for="name_{{view.id}}" i18n>Name</label> <label class="form-label" for="name_{{view.id}}" i18n>Name</label>
<input type="text" class="form-control" formControlName="name" id="name_{{view.id}}"> <input type="text" class="form-control" formControlName="name" id="name_{{view.id}}">
</div> </div>
<div class="mb-2 col"> <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> <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"> <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> <label class="form-check-label" for="show_in_sidebar_{{view.id}}" i18n>Show in sidebar</label>
</div> </div>
</div> </div>
<div class="mb-2 col-auto"> <div class="mb-2 col-auto">
<label class="form-label" for="name_{{view.id}}" i18n>Actions</label> <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> <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> </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="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div> <div class="visually-hidden" i18n>Loading...</div>
</div> </div>
}
</div> </div>

View File

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

View File

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

View File

@ -17,10 +17,10 @@
</div> </div>
</pngx-page-header> </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="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div> <div class="visually-hidden" i18n>Loading...</div>
</ng-container> }
<ng-template let-tasks="tasks" #tasksTemplate> <ng-template let-tasks="tasks" #tasksTemplate>
<table class="table table-striped align-middle border shadow-sm"> <table class="table table-striped align-middle border shadow-sm">
@ -34,13 +34,15 @@
</th> </th>
<th scope="col" i18n>Name</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" 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" class="d-table-cell d-lg-none" i18n>Info</th>
<th scope="col" i18n>Actions</th> <th scope="col" i18n>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <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();"> <tr (click)="toggleSelected(task, $event); $event.stopPropagation();">
<td> <td>
<div class="form-check"> <div class="form-check">
@ -50,17 +52,27 @@
</td> </td>
<td class="overflow-auto name-col">{{ task.task_file_name }}</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">{{ task.date_created | customDate:'short' }}</td>
<td class="d-none d-lg-table-cell" *ngIf="activeTab !== 'started' && activeTab !== 'queued'"> @if (activeTab !== 'started' && activeTab !== 'queued') {
<div *ngIf="task.result?.length > 50" class="result" (click)="expandTask(task); $event.stopPropagation();" <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"> [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> <span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result | slice:0:50 }}&hellip;</span>
</div> </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> <ng-template #resultPopover>
<pre class="small mb-0">{{ task.result | slice:0:300 }}<ng-container *ngIf="task.result.length > 300">&hellip;</ng-container></pre> <pre class="small mb-0">{{ task.result | slice:0:300 }}@if (task.result.length > 300) {
<ng-container *ngIf="task.result?.length > 300"><br/><em>(<ng-container i18n>click for full output</ng-container>)</em></ng-container> &hellip;
}</pre>
@if (task.result?.length > 300) {
<br/><em>(<ng-container i18n>click for full output</ng-container>)</em>
}
</ng-template> </ng-template>
</td> </td>
}
<td class="d-lg-none"> <td class="d-lg-none">
<button class="btn btn-link" (click)="expandTask(task); $event.stopPropagation();"> <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"> <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> </svg>&nbsp;<ng-container i18n>Dismiss</ng-container>
</button> </button>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"> <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"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-text"/> <use xlink:href="assets/bootstrap-icons.svg#file-text"/>
</svg>&nbsp;<ng-container i18n>Open Document</ng-container> </svg>&nbsp;<ng-container i18n>Open Document</ng-container>
</button> </button>
}
</ng-container> </ng-container>
</div> </div>
</td> </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> <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> </td>
</tr> </tr>
</ng-container> }
</tbody> </tbody>
</table> </table>
<div class="pb-3 d-sm-flex justify-content-between align-items-center"> <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> @if (tasks.length > 0) {
<ngb-pagination *ngIf="tasks.length > pageSize" [(page)]="page" [pageSize]="pageSize" [collectionSize]="tasks.length" maxSize="8" size="sm"></ngb-pagination> <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> </div>
</ng-template> </ng-template>
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" (hidden)="duringTabChange($event)"> <ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" (hidden)="duringTabChange($event)">
<li ngbNavItem="failed"> <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-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.failedFileTasks}"></ng-container> <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.failedFileTasks}"></ng-container>
</ng-template> </ng-template>
</li> </li>
<li ngbNavItem="completed"> <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-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.completedFileTasks}"></ng-container> <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.completedFileTasks}"></ng-container>
</ng-template> </ng-template>
</li> </li>
<li ngbNavItem="started"> <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-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.startedFileTasks}"></ng-container> <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.startedFileTasks}"></ng-container>
</ng-template> </ng-template>
</li> </li>
<li ngbNavItem="queued"> <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-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.queuedFileTasks}"></ng-container> <ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.queuedFileTasks}"></ng-container>
</ng-template> </ng-template>

View File

@ -1,7 +1,7 @@
<pngx-page-header title="Users & Groups" i18n-title> <pngx-page-header title="Users & Groups" i18n-title>
</pngx-page-header> </pngx-page-header>
<ng-container *ngIf="users"> @if (users) {
<h4 class="d-flex"> <h4 class="d-flex">
<ng-container i18n>Users</ng-container> <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 }"> <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 class="col" i18n>Actions</div>
</div> </div>
</li> </li>
@for (user of users; track user) {
<li *ngFor="let user of users" class="list-group-item"> <li class="list-group-item">
<div class="row"> <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"><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> <div class="col d-flex align-items-center">{{user.first_name}} {{user.last_name}}</div>
@ -42,10 +42,11 @@
</div> </div>
</div> </div>
</li> </li>
}
</ul> </ul>
</ng-container> }
<ng-container *ngIf="groups"> @if (groups) {
<h4 class="mt-4 d-flex"> <h4 class="mt-4 d-flex">
<ng-container i18n>Groups</ng-container> <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 }"> <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> <ng-container i18n>Add Group</ng-container>
</button> </button>
</h4> </h4>
<ul *ngIf="groups.length > 0" class="list-group"> @if (groups.length > 0) {
<ul class="list-group">
<li class="list-group-item"> <li class="list-group-item">
<div class="row"> <div class="row">
<div class="col" i18n>Name</div> <div class="col" i18n>Name</div>
@ -64,8 +66,8 @@
<div class="col" i18n>Actions</div> <div class="col" i18n>Actions</div>
</div> </div>
</li> </li>
@for (group of groups; track group) {
<li *ngFor="let group of groups" class="list-group-item"> <li class="list-group-item">
<div class="row"> <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 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> <div class="col"></div>
@ -86,12 +88,17 @@
</div> </div>
</div> </div>
</li> </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> </ul>
}
}
</ng-container> @if (!users || !groups) {
<div>
<div *ngIf="!users || !groups">
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></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 class="visually-hidden" i18n>Loading...</div>
</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 { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { SettingsComponent } from '../settings/settings.component' import { SettingsComponent } from '../settings/settings.component'
import { UsersAndGroupsComponent } from './users-groups.component' import { UsersAndGroupsComponent } from './users-groups.component'
import { PaperlessUser } from 'src/app/data/paperless-user' import { User } from 'src/app/data/user'
import { PaperlessGroup } from 'src/app/data/paperless-group' import { Group } from 'src/app/data/group'
const users = [ const users = [
{ id: 1, username: 'user1', is_superuser: false }, { id: 1, username: 'user1', is_superuser: false },
@ -119,7 +119,7 @@ describe('UsersAndGroupsComponent', () => {
of({ of({
all: users.map((a) => a.id), all: users.map((a) => a.id),
count: users.length, count: users.length,
results: (users as PaperlessUser[]).concat([]), results: (users as User[]).concat([]),
}) })
) )
} }
@ -128,7 +128,7 @@ describe('UsersAndGroupsComponent', () => {
of({ of({
all: groups.map((r) => r.id), all: groups.map((r) => r.id),
count: groups.length, 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 { Component, OnDestroy, OnInit } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { Subject, first, takeUntil } from 'rxjs' import { Subject, first, takeUntil } from 'rxjs'
import { PaperlessGroup } from 'src/app/data/paperless-group' import { Group } from 'src/app/data/group'
import { PaperlessUser } from 'src/app/data/paperless-user' import { User } from 'src/app/data/user'
import { PermissionsService } from 'src/app/services/permissions.service' import { PermissionsService } from 'src/app/services/permissions.service'
import { GroupService } from 'src/app/services/rest/group.service' import { GroupService } from 'src/app/services/rest/group.service'
import { UserService } from 'src/app/services/rest/user.service' import { UserService } from 'src/app/services/rest/user.service'
@ -23,8 +23,8 @@ export class UsersAndGroupsComponent
extends ComponentWithPermissions extends ComponentWithPermissions
implements OnInit, OnDestroy implements OnInit, OnDestroy
{ {
users: PaperlessUser[] users: User[]
groups: PaperlessGroup[] groups: Group[]
unsubscribeNotifier: Subject<any> = new Subject() unsubscribeNotifier: Subject<any> = new Subject()
@ -69,7 +69,7 @@ export class UsersAndGroupsComponent
this.unsubscribeNotifier.next(true) this.unsubscribeNotifier.next(true)
} }
editUser(user: PaperlessUser = null) { editUser(user: User = null) {
var modal = this.modalService.open(UserEditDialogComponent, { var modal = this.modalService.open(UserEditDialogComponent, {
backdrop: 'static', backdrop: 'static',
size: 'xl', size: 'xl',
@ -80,7 +80,7 @@ export class UsersAndGroupsComponent
modal.componentInstance.object = user modal.componentInstance.object = user
modal.componentInstance.succeeded modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier)) .pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((newUser: PaperlessUser) => { .subscribe((newUser: User) => {
if ( if (
newUser.id === this.settings.currentUser.id && newUser.id === this.settings.currentUser.id &&
(modal.componentInstance as UserEditDialogComponent).passwordIsSet (modal.componentInstance as UserEditDialogComponent).passwordIsSet
@ -107,7 +107,7 @@ export class UsersAndGroupsComponent
}) })
} }
deleteUser(user: PaperlessUser) { deleteUser(user: User) {
let modal = this.modalService.open(ConfirmDialogComponent, { let modal = this.modalService.open(ConfirmDialogComponent, {
backdrop: 'static', backdrop: 'static',
}) })
@ -133,7 +133,7 @@ export class UsersAndGroupsComponent
}) })
} }
editGroup(group: PaperlessGroup = null) { editGroup(group: Group = null) {
var modal = this.modalService.open(GroupEditDialogComponent, { var modal = this.modalService.open(GroupEditDialogComponent, {
backdrop: 'static', backdrop: 'static',
size: 'lg', size: 'lg',
@ -157,7 +157,7 @@ export class UsersAndGroupsComponent
}) })
} }
deleteGroup(group: PaperlessGroup) { deleteGroup(group: Group) {
let modal = this.modalService.open(ConfirmDialogComponent, { let modal = this.modalService.open(ConfirmDialogComponent, {
backdrop: 'static', backdrop: 'static',
}) })

View File

@ -4,24 +4,32 @@
(click)="isMenuCollapsed = !isMenuCollapsed"> (click)="isMenuCollapsed = !isMenuCollapsed">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </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"> <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> </svg>
<span class="ms-2" [class.visually-hidden]="slimSidebarEnabled" i18n="app title">Paperless-ngx</span> <span class="ms-2" [class.visually-hidden]="slimSidebarEnabled" i18n="app title">Paperless-ngx</span>
</a> </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"> <form (ngSubmit)="search()" class="form-inline flex-grow-1">
<svg width="1em" height="1em" fill="currentColor"> <svg width="1em" height="1em" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#search" /> <use xlink:href="assets/bootstrap-icons.svg#search" />
</svg> </svg>
<input class="form-control form-control-sm" type="text" placeholder="Search documents" aria-label="Search" <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> [formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (keyup)="searchFieldKeyup($event)"
<button type="button" *ngIf="!searchFieldEmpty" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0" (click)="resetSearchField()"> (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"> <svg fill="currentColor" class="buttonicon-sm me-1">
<use xlink:href="assets/bootstrap-icons.svg#x" /> <use xlink:href="assets/bootstrap-icons.svg#x" />
</svg> </svg>
</button> </button>
}
</form> </form>
</div> </div>
<ul ngbNav class="order-sm-3"> <ul ngbNav class="order-sm-3">
@ -44,7 +52,8 @@
<use xlink:href="assets/bootstrap-icons.svg#person" /> <use xlink:href="assets/bootstrap-icons.svg#person" />
</svg><ng-container i18n>My Profile</ng-container> </svg><ng-container i18n>My Profile</ng-container>
</button> </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"> <svg class="sidebaricon me-2" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#gear" /> <use xlink:href="assets/bootstrap-icons.svg#gear" />
</svg><ng-container i18n>Settings</ng-container> </svg><ng-container i18n>Settings</ng-container>
@ -55,7 +64,8 @@
</svg><ng-container i18n>Logout</ng-container> </svg><ng-container i18n>Logout</ng-container>
</a> </a>
<div class="dropdown-divider"></div> <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"> <svg class="sidebaricon me-2" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#question-circle" /> <use xlink:href="assets/bootstrap-icons.svg#question-circle" />
</svg><ng-container i18n>Documentation</ng-container> </svg><ng-container i18n>Documentation</ng-container>
@ -67,24 +77,33 @@
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <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()"> <button class="btn btn-sm btn-dark sidebar-slim-toggler" (click)="toggleSlimSidebar()">
<svg class="sidebaricon-sm" fill="currentColor"> <svg class="sidebaricon-sm" fill="currentColor">
<use *ngIf="slimSidebarEnabled" xlink:href="assets/bootstrap-icons.svg#chevron-double-right"/> @if (slimSidebarEnabled) {
<use *ngIf="!slimSidebarEnabled" xlink:href="assets/bootstrap-icons.svg#chevron-double-left"/> <use xlink:href="assets/bootstrap-icons.svg#chevron-double-right" />
} @else {
<use xlink:href="assets/bootstrap-icons.svg#chevron-double-left" />
}
</svg> </svg>
</button> </button>
<div class="sidebar-sticky pt-3 d-flex flex-column justify-space-around"> <div class="sidebar-sticky pt-3 d-flex flex-column justify-space-around">
<ul class="nav flex-column"> <ul class="nav flex-column">
<li class="nav-item"> <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"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#house" /> <use xlink:href="assets/bootstrap-icons.svg#house" />
</svg><span>&nbsp;<ng-container i18n>Dashboard</ng-container></span> </svg><span>&nbsp;<ng-container i18n>Dashboard</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"> <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"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#files" /> <use xlink:href="assets/bootstrap-icons.svg#files" />
</svg><span>&nbsp;<ng-container i18n>Documents</ng-container></span> </svg><span>&nbsp;<ng-container i18n>Documents</ng-container></span>
@ -92,39 +111,52 @@
</li> </li>
</ul> </ul>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }"> <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> <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> </h6>
}
<ul class="nav flex-column mb-2" cdkDropList (cdkDropListDropped)="onDrop($event)"> <ul class="nav flex-column mb-2" cdkDropList (cdkDropListDropped)="onDrop($event)">
<li class="nav-item w-100" *ngFor="let view of savedViewService.sidebarViews" @for (view of savedViewService.sidebarViews; track view) {
cdkDrag <li class="nav-item w-100" cdkDrag [cdkDragDisabled]="!settingsService.organizingSidebarSavedViews"
[cdkDragDisabled]="!settingsService.organizingSidebarSavedViews" cdkDragPreviewContainer="parent" cdkDragPreviewClass="navItemDrag" (cdkDragStarted)="onDragStart($event)"
cdkDragPreviewContainer="parent"
cdkDragPreviewClass="navItemDrag"
(cdkDragStarted)="onDragStart($event)"
(cdkDragEnded)="onDragEnd($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"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#funnel" /> <use xlink:href="assets/bootstrap-icons.svg#funnel" />
</svg><span>&nbsp;{{view.name}}</span> </svg><span>&nbsp;{{view.name}}</span>
</a> </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"> <svg class="sidebaricon text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#grip-vertical" /> <use xlink:href="assets/bootstrap-icons.svg#grip-vertical" />
</svg> </svg>
</div> </div>
}
</li> </li>
}
</ul> </ul>
</ng-container> </ng-container>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"> <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> <span i18n>Open documents</span>
</h6> </h6>
}
<ul class="nav flex-column mb-2"> <ul class="nav flex-column mb-2">
<li class="nav-item w-100" *ngFor='let d of openDocuments'> @for (d of openDocuments; track d) {
<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"> <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"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-text" /> <use xlink:href="assets/bootstrap-icons.svg#file-text" />
</svg><span>&nbsp;{{d.title | documentTitle}}</span> </svg><span>&nbsp;{{d.title | documentTitle}}</span>
@ -135,13 +167,18 @@
</span> </span>
</a> </a>
</li> </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"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x" /> <use xlink:href="assets/bootstrap-icons.svg#x" />
</svg><span>&nbsp;<ng-container i18n>Close all</ng-container></span> </svg><span>&nbsp;<ng-container i18n>Close all</ng-container></span>
</a> </a>
</li> </li>
}
</ul> </ul>
</ng-container> </ng-container>
@ -149,50 +186,70 @@
<span i18n>Manage</span> <span i18n>Manage</span>
</h6> </h6>
<ul class="nav flex-column mb-2"> <ul class="nav flex-column mb-2">
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }"> <li class="nav-item"
<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"> *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"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person" /> <use xlink:href="assets/bootstrap-icons.svg#person" />
</svg><span>&nbsp;<ng-container i18n>Correspondents</ng-container></span> </svg><span>&nbsp;<ng-container i18n>Correspondents</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }" tourAnchor="tour.tags"> <li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"
<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"> 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"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#tags" /> <use xlink:href="assets/bootstrap-icons.svg#tags" />
</svg><span>&nbsp;<ng-container i18n>Tags</ng-container></span> </svg><span>&nbsp;<ng-container i18n>Tags</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }"> <li class="nav-item"
<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"> *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"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#hash" /> <use xlink:href="assets/bootstrap-icons.svg#hash" />
</svg><span>&nbsp;<ng-container i18n>Document Types</ng-container></span> </svg><span>&nbsp;<ng-container i18n>Document Types</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }"> <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"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#folder" /> <use xlink:href="assets/bootstrap-icons.svg#folder" />
</svg><span>&nbsp;<ng-container i18n>Storage Paths</ng-container></span> </svg><span>&nbsp;<ng-container i18n>Storage Paths</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }"> <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"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#ui-radios" /> <use xlink:href="assets/bootstrap-icons.svg#ui-radios" />
</svg><span>&nbsp;<ng-container i18n>Custom Fields</ng-container></span> </svg><span>&nbsp;<ng-container i18n>Custom Fields</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.ConsumptionTemplate }" tourAnchor="tour.consumption-templates"> <li class="nav-item"
<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"> *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"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-earmark-ruled" /> <use xlink:href="assets/bootstrap-icons.svg#file-earmark-ruled" />
</svg><span>&nbsp;<ng-container i18n>Templates</ng-container></span> </svg><span>&nbsp;<ng-container i18n>Templates</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailAccount }" tourAnchor="tour.mail"> <li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailAccount }"
<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"> 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"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#envelope" /> <use xlink:href="assets/bootstrap-icons.svg#envelope" />
</svg><span>&nbsp;<ng-container i18n>Mail</ng-container></span> </svg><span>&nbsp;<ng-container i18n>Mail</ng-container></span>
@ -204,37 +261,55 @@
<span i18n>Administration</span> <span i18n>Administration</span>
</h6> </h6>
<ul class="nav flex-column mb-2"> <ul class="nav flex-column mb-2">
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }" tourAnchor="tour.settings"> <li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }"
<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"> 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"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#gear" /> <use xlink:href="assets/bootstrap-icons.svg#gear" />
</svg><span>&nbsp;<ng-container i18n>Settings</ng-container></span> </svg><span>&nbsp;<ng-container i18n>Settings</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }"> <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"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#people" /> <use xlink:href="assets/bootstrap-icons.svg#people" />
</svg><span>&nbsp;<ng-container i18n>Users & Groups</ng-container></span> </svg><span>&nbsp;<ng-container i18n>Users & Groups</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.PaperlessTask }" tourAnchor="tour.file-tasks"> <li class="nav-item"
<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"> *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.PaperlessTask }"
<span *ngIf="tasksService.failedFileTasks.length > 0 && slimSidebarEnabled" class="badge bg-danger position-absolute top-0 end-0">{{tasksService.failedFileTasks.length}}</span> 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"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#list-task" /> <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> </a>
</li> </li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }"> <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"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#text-left" /> <use xlink:href="assets/bootstrap-icons.svg#text-left" />
</svg><span>&nbsp;<ng-container i18n>Logs</ng-container></span> </svg><span>&nbsp;<ng-container i18n>Logs</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item mt-2" tourAnchor="tour.outro"> <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"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#question-circle" /> <use xlink:href="assets/bootstrap-icons.svg#question-circle" />
</svg><span class="ms-1">&nbsp;<ng-container i18n>Documentation</ng-container></span> </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"> <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="px-3 py-0 text-muted small d-flex align-items-center flex-wrap">
<div class="me-3"> <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 }} {{ versionString }}
</a> </a>
</div> </div>
<div *ngIf="!settingsService.updateCheckingIsSet || appRemoteVersion" class="version-check"> @if (!settingsService.updateCheckingIsSet || appRemoteVersion) {
<div class="version-check">
<ng-template #updateAvailablePopContent> <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>
<ng-template #updateCheckingNotEnabledPopContent> <ng-template #updateCheckingNotEnabledPopContent>
<p class="small mb-2"> <p class="small mb-2">
@ -265,31 +345,41 @@
</a> </a>
</p> </p>
</ng-template> </ng-template>
<ng-container *ngIf="settingsService.updateCheckingIsSet; else updateCheckNotSet"> @if (settingsService.updateCheckingIsSet) {
<a *ngIf="appRemoteVersion.update_available" class="small text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx/releases" @if (appRemoteVersion.update_available) {
[ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave" container="body"> <a class="small text-decoration-none" target="_blank" rel="noopener noreferrer"
<svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16"> 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" /> <use xlink:href="assets/bootstrap-icons.svg#info-circle" />
</svg> </svg>
<ng-container *ngIf="appRemoteVersion?.update_available" i18n>Update available</ng-container> @if (appRemoteVersion?.update_available) {
<ng-container i18n>Update available</ng-container>
}
</a> </a>
</ng-container> }
<ng-template #updateCheckNotSet> } @else {
<a class="small text-decoration-none" routerLink="/settings" fragment="update-checking" <a class="small text-decoration-none" routerLink="/settings" fragment="update-checking"
[ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter" container="body"> [ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter"
<svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16"> 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" /> <use xlink:href="assets/bootstrap-icons.svg#info-circle" />
</svg> </svg>
</a> </a>
</ng-template> }
</div> </div>
}
</div> </div>
</li> </li>
</ul> </ul>
</div> </div>
</nav> </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> <router-outlet></router-outlet>
</main> </main>
</div> </div>

View File

@ -15,11 +15,11 @@ import { RouterTestingModule } from '@angular/router/testing'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { PermissionsService } from 'src/app/services/permissions.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 { RemoteVersionService } from 'src/app/services/rest/remote-version.service'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' 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 { ToastService } from 'src/app/services/toast.service'
import { environment } from 'src/environments/environment' import { environment } from 'src/environments/environment'
import { OpenDocumentsService } from 'src/app/services/open-documents.service' 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 { routes } from 'src/app/app-routing.module'
import { PermissionsGuard } from 'src/app/guards/permissions.guard' import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop' 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' import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
const saved_views = [ const saved_views = [
@ -356,7 +356,7 @@ describe('AppFrameComponent', () => {
const toastSpy = jest.spyOn(toastService, 'showInfo') const toastSpy = jest.spyOn(toastService, 'showInfo')
jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true)) jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true))
component.onDrop({ previousIndex: 0, currentIndex: 1 } as CdkDragDrop< component.onDrop({ previousIndex: 0, currentIndex: 1 } as CdkDragDrop<
PaperlessSavedView[] SavedView[]
>) >)
expect(settingsSpy).toHaveBeenCalledWith([ expect(settingsSpy).toHaveBeenCalledWith([
saved_views[2], saved_views[2],
@ -379,7 +379,7 @@ describe('AppFrameComponent', () => {
.spyOn(settingsService, 'storeSettings') .spyOn(settingsService, 'storeSettings')
.mockReturnValue(throwError(() => new Error('unable to save'))) .mockReturnValue(throwError(() => new Error('unable to save')))
component.onDrop({ previousIndex: 0, currentIndex: 2 } as CdkDragDrop< component.onDrop({ previousIndex: 0, currentIndex: 2 } as CdkDragDrop<
PaperlessSavedView[] SavedView[]
>) >)
expect(toastSpy).toHaveBeenCalled() expect(toastSpy).toHaveBeenCalled()
}) })

View File

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

View File

@ -4,8 +4,12 @@
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p *ngIf="messageBold"><b>{{messageBold}}</b></p> @if (messageBold) {
<p class="mb-0" *ngIf="message" [innerHTML]="message | safeHtml"></p> <p><b>{{messageBold}}</b></p>
}
@if (message) {
<p class="mb-0" [innerHTML]="message | safeHtml"></p>
}
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" [disabled]="!buttonsEnabled" i18n> <button type="button" class="btn btn-outline-secondary" (click)="cancel()" [disabled]="!buttonsEnabled" i18n>
@ -16,9 +20,13 @@
{{btnCaption}} {{btnCaption}}
<span class="visually-hidden">{{ seconds | number: '1.0-0' }} seconds</span> <span class="visually-hidden">{{ seconds | number: '1.0-0' }} seconds</span>
</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>
<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}} {{alternativeBtnCaption}}
</button> </button>
}
</div> </div>

View File

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

View File

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

View File

@ -5,11 +5,14 @@
</button> </button>
<div class="dropdown-menu date-dropdown shadow pt-0" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> <div class="dropdown-menu date-dropdown shadow pt-0" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
<div class="list-group list-group-flush"> <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"> <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"/> <use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg> </svg>
}
</div> </div>
<div class="d-flex justify-content-between w-100 align-items-center ps-2"> <div class="d-flex justify-content-between w-100 align-items-center ps-2">
<div class="pe-2 pe-lg-4"> <div class="pe-2 pe-lg-4">
@ -22,16 +25,19 @@
</div> </div>
</div> </div>
</button> </button>
}
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem"> <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 class="mb-2 d-flex flex-row w-100 justify-content-between small">
<div i18n>After</div> <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"> <svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg> </svg>
<small i18n>Clear</small> <small i18n>Clear</small>
</a> </a>
}
</div> </div>
<div class="input-group input-group-sm"> <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 class="mb-2 d-flex flex-row w-100 justify-content-between small">
<div i18n>Before</div> <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"> <svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg> </svg>
<small i18n>Clear</small> <small i18n>Clear</small>
</a> </a>
}
</div> </div>
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">

View File

@ -86,7 +86,9 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <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="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> <button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
</div> </div>

View File

@ -4,11 +4,11 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { first } from 'rxjs' import { first } from 'rxjs'
import { import {
DocumentSource, DocumentSource,
PaperlessConsumptionTemplate, ConsumptionTemplate,
} from 'src/app/data/paperless-consumption-template' } from 'src/app/data/consumption-template'
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent' import { Correspondent } from 'src/app/data/correspondent'
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type' import { DocumentType } from 'src/app/data/document-type'
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path' import { StoragePath } from 'src/app/data/storage-path'
import { ConsumptionTemplateService } from 'src/app/services/rest/consumption-template.service' import { ConsumptionTemplateService } from 'src/app/services/rest/consumption-template.service'
import { CorrespondentService } from 'src/app/services/rest/correspondent.service' import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
import { DocumentTypeService } from 'src/app/services/rest/document-type.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 { SettingsService } from 'src/app/services/settings.service'
import { EditDialogComponent } from '../edit-dialog.component' import { EditDialogComponent } from '../edit-dialog.component'
import { MailRuleService } from 'src/app/services/rest/mail-rule.service' 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 { 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 = [ export const DOCUMENT_SOURCE_OPTIONS = [
{ {
@ -41,13 +41,13 @@ export const DOCUMENT_SOURCE_OPTIONS = [
templateUrl: './consumption-template-edit-dialog.component.html', templateUrl: './consumption-template-edit-dialog.component.html',
styleUrls: ['./consumption-template-edit-dialog.component.scss'], styleUrls: ['./consumption-template-edit-dialog.component.scss'],
}) })
export class ConsumptionTemplateEditDialogComponent extends EditDialogComponent<PaperlessConsumptionTemplate> { export class ConsumptionTemplateEditDialogComponent extends EditDialogComponent<ConsumptionTemplate> {
templates: PaperlessConsumptionTemplate[] templates: ConsumptionTemplate[]
correspondents: PaperlessCorrespondent[] correspondents: Correspondent[]
documentTypes: PaperlessDocumentType[] documentTypes: DocumentType[]
storagePaths: PaperlessStoragePath[] storagePaths: StoragePath[]
mailRules: PaperlessMailRule[] mailRules: MailRule[]
customFields: PaperlessCustomField[] customFields: CustomField[]
constructor( constructor(
service: ConsumptionTemplateService, 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-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-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> @if (patternRequired) {
<pngx-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive" novalidate></pngx-input-check> <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"> <div *pngxIfOwner="object">
<pngx-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></pngx-permissions-form> <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 { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' 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 { CorrespondentService } from 'src/app/services/rest/correspondent.service'
import { UserService } from 'src/app/services/rest/user.service' import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.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', templateUrl: './correspondent-edit-dialog.component.html',
styleUrls: ['./correspondent-edit-dialog.component.scss'], styleUrls: ['./correspondent-edit-dialog.component.scss'],
}) })
export class CorrespondentEditDialogComponent extends EditDialogComponent<PaperlessCorrespondent> { export class CorrespondentEditDialogComponent extends EditDialogComponent<Correspondent> {
constructor( constructor(
service: CorrespondentService, service: CorrespondentService,
activeModal: NgbActiveModal, activeModal: NgbActiveModal,

View File

@ -7,7 +7,9 @@
<div class="modal-body"> <div class="modal-body">
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text> <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> <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>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> <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 { Component, OnInit } from '@angular/core'
import { FormGroup, FormControl } from '@angular/forms' import { FormGroup, FormControl } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { import { DATA_TYPE_LABELS, CustomField } from 'src/app/data/custom-field'
DATA_TYPE_LABELS,
PaperlessCustomField,
} from 'src/app/data/paperless-custom-field'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { UserService } from 'src/app/services/rest/user.service' import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.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'], styleUrls: ['./custom-field-edit-dialog.component.scss'],
}) })
export class CustomFieldEditDialogComponent export class CustomFieldEditDialogComponent
extends EditDialogComponent<PaperlessCustomField> extends EditDialogComponent<CustomField>
implements OnInit implements OnInit
{ {
constructor( constructor(

View File

@ -9,8 +9,12 @@
<div class="col"> <div class="col">
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text> <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-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> @if (patternRequired) {
<pngx-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check> <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>
<div *pngxIfOwner="object"> <div *pngxIfOwner="object">

View File

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

View File

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

View File

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

View File

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

View File

@ -22,13 +22,15 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<div class="m-0 me-2"> <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> </div>
<button type="button" class="btn btn-outline-primary" (click)="test()" [disabled]="networkActive || testActive"> <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> <div class="spinner-border spinner-border-sm me-2" role="status"></div>
<span class="visually-hidden mr-1" i18n>Loading...</span> <span class="visually-hidden mr-1" i18n>Loading...</span>
</ng-container> }
<ng-container i18n>Test</ng-container> <ng-container i18n>Test</ng-container>
</button> </button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</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 { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select' 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 { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SettingsService } from 'src/app/services/settings.service' 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 { FormControl, FormGroup } from '@angular/forms'
import { NgbActiveModal, NgbAlert } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal, NgbAlert } from '@ng-bootstrap/ng-bootstrap'
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
import { import { IMAPSecurity, MailAccount } from 'src/app/data/mail-account'
IMAPSecurity,
PaperlessMailAccount,
} from 'src/app/data/paperless-mail-account'
import { MailAccountService } from 'src/app/services/rest/mail-account.service' import { MailAccountService } from 'src/app/services/rest/mail-account.service'
import { UserService } from 'src/app/services/rest/user.service' import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
@ -21,7 +18,7 @@ const IMAP_SECURITY_OPTIONS = [
templateUrl: './mail-account-edit-dialog.component.html', templateUrl: './mail-account-edit-dialog.component.html',
styleUrls: ['./mail-account-edit-dialog.component.scss'], styleUrls: ['./mail-account-edit-dialog.component.scss'],
}) })
export class MailAccountEditDialogComponent extends EditDialogComponent<PaperlessMailAccount> { export class MailAccountEditDialogComponent extends EditDialogComponent<MailAccount> {
testActive: boolean = false testActive: boolean = false
testResult: string testResult: string
alertTimeout alertTimeout

View File

@ -26,19 +26,25 @@
</div> </div>
<div class="col-md-4"> <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-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> <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-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-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 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 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> <pngx-input-check i18n-title title="Assign owner from rule" formControlName="assign_owner_from_rule"></pngx-input-check>
</div> </div>
</div> </div>
</div> </div>
<div class="modal-footer"> <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="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> <button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
</div> </div>

View File

@ -7,7 +7,7 @@ import { of } from 'rxjs'
import { import {
MailMetadataCorrespondentOption, MailMetadataCorrespondentOption,
MailAction, MailAction,
} from 'src/app/data/paperless-mail-rule' } from 'src/app/data/mail-rule'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive' import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe' 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 { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { first } from 'rxjs' import { first } from 'rxjs'
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent' import { Correspondent } from 'src/app/data/correspondent'
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type' import { DocumentType } from 'src/app/data/document-type'
import { PaperlessMailAccount } from 'src/app/data/paperless-mail-account' import { MailAccount } from 'src/app/data/mail-account'
import { import {
MailAction, MailAction,
MailFilterAttachmentType, MailFilterAttachmentType,
MailMetadataCorrespondentOption, MailMetadataCorrespondentOption,
MailMetadataTitleOption, MailMetadataTitleOption,
PaperlessMailRule, MailRule,
MailRuleConsumptionScope, MailRuleConsumptionScope,
} from 'src/app/data/paperless-mail-rule' } from 'src/app/data/mail-rule'
import { CorrespondentService } from 'src/app/services/rest/correspondent.service' import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
import { DocumentTypeService } from 'src/app/services/rest/document-type.service' import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
import { MailAccountService } from 'src/app/services/rest/mail-account.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', templateUrl: './mail-rule-edit-dialog.component.html',
styleUrls: ['./mail-rule-edit-dialog.component.scss'], styleUrls: ['./mail-rule-edit-dialog.component.scss'],
}) })
export class MailRuleEditDialogComponent extends EditDialogComponent<PaperlessMailRule> { export class MailRuleEditDialogComponent extends EditDialogComponent<MailRule> {
accounts: PaperlessMailAccount[] accounts: MailAccount[]
correspondents: PaperlessCorrespondent[] correspondents: Correspondent[]
documentTypes: PaperlessDocumentType[] documentTypes: DocumentType[]
constructor( constructor(
service: MailRuleService, 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="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-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-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> @if (patternRequired) {
<pngx-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check> <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"> <div *pngxIfOwner="object">
<pngx-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></pngx-permissions-form> <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 { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' 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 { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { UserService } from 'src/app/services/rest/user.service' import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.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', templateUrl: './storage-path-edit-dialog.component.html',
styleUrls: ['./storage-path-edit-dialog.component.scss'], styleUrls: ['./storage-path-edit-dialog.component.scss'],
}) })
export class StoragePathEditDialogComponent extends EditDialogComponent<PaperlessStoragePath> { export class StoragePathEditDialogComponent extends EditDialogComponent<StoragePath> {
constructor( constructor(
service: StoragePathService, service: StoragePathService,
activeModal: NgbActiveModal, 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-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-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> @if (patternRequired) {
<pngx-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check> <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"> <div *pngxIfOwner="object">
<pngx-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></pngx-permissions-form> <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 { FormControl, FormGroup } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' 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 { TagService } from 'src/app/services/rest/tag.service'
import { randomColor } from 'src/app/utils/color' import { randomColor } from 'src/app/utils/color'
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' 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', templateUrl: './tag-edit-dialog.component.html',
styleUrls: ['./tag-edit-dialog.component.scss'], styleUrls: ['./tag-edit-dialog.component.scss'],
}) })
export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> { export class TagEditDialogComponent extends EditDialogComponent<Tag> {
constructor( constructor(
service: TagService, service: TagService,
activeModal: NgbActiveModal, activeModal: NgbActiveModal,

View File

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

View File

@ -4,13 +4,14 @@
<use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" /> <use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
</svg> </svg>
<div class="d-none d-sm-inline">&nbsp;{{title}}</div> <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> <pngx-clearable-badge [number]="selectionModel.totalCount" [selected]="selectionModel.selectionSize() > 0" (cleared)="reset()"></pngx-clearable-badge>
</ng-container> }
</button> </button>
<div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown_{{name}}"> <div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown_{{name}}">
<div class="list-group list-group-flush"> <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"> <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"> <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> <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> <label class="btn btn-outline-primary" for="logicalOperatorOr_{{name}}" i18n>Any</label>
</div> </div>
</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"> <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"> <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> <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> <label class="btn btn-outline-primary" for="intersectionExclude_{{name}}" i18n>Exclude</label>
</div> </div>
</div> </div>
}
<div class="list-group-item"> <div class="list-group-item">
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<input class="form-control" type="text" [(ngModel)]="filterText" [placeholder]="filterPlaceholder" (keyup.enter)="listFilterEnter()" #listFilterTextInput> <input class="form-control" type="text" [(ngModel)]="filterText" [placeholder]="filterPlaceholder" (keyup.enter)="listFilterEnter()" #listFilterTextInput>
</div> </div>
</div> </div>
<div *ngIf="selectionModel.items" class="items" #buttonItems> @if (selectionModel.items) {
<ng-container *ngFor="let item of selectionModel.itemsSorted | filter: filterText; let i = index"> <div class="items" #buttonItems>
@for (item of selectionModel.itemsSorted | filter: filterText; track item; let i = $index) {
@if (allowSelectNone || item.id) {
<pngx-toggleable-dropdown-button <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> </pngx-toggleable-dropdown-button>
</ng-container> }
}
</div> </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> <small class="ms-2" [ngClass]="{'fw-bold': modelIsDirty}" i18n>Apply</small>
<svg width="1.5em" height="1em" viewBox="0 0 16 16" fill="currentColor"> <svg width="1.5em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#arrow-right" /> <use xlink:href="assets/bootstrap-icons.svg#arrow-right" />
</svg> </svg>
</button> </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> <small i18n>Click again to exclude items.</small>
</div> </div>
}
</div> </div>
</div> </div>
</div> </div>

View File

@ -13,7 +13,7 @@ import {
} from './filterable-dropdown.component' } from './filterable-dropdown.component'
import { FilterPipe } from 'src/app/pipes/filter.pipe' import { FilterPipe } from 'src/app/pipes/filter.pipe'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { PaperlessTag } from 'src/app/data/paperless-tag' import { Tag } from 'src/app/data/tag'
import { import {
DEFAULT_MATCHING_ALGORITHM, DEFAULT_MATCHING_ALGORITHM,
MATCH_ALL, MATCH_ALL,
@ -26,7 +26,7 @@ import { TagComponent } from '../tag/tag.component'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component' import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
const items: PaperlessTag[] = [ const items: Tag[] = [
{ {
id: 1, id: 1,
name: 'Tag1', 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"> <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"> <div class="selected-icon me-1">
<ng-container *ngIf="isChecked()"> @if (isChecked()) {
<svg fill="currentColor" class="buttonicon-sm bi-check"> <svg fill="currentColor" class="buttonicon-sm bi-check">
<use xlink:href="assets/bootstrap-icons.svg#check"/> <use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg> </svg>
</ng-container> }
<ng-container *ngIf="isPartiallyChecked()"> @if (isPartiallyChecked()) {
<svg fill="currentColor" class="buttonicon-sm bi-dash"> <svg fill="currentColor" class="buttonicon-sm bi-dash">
<use xlink:href="assets/bootstrap-icons.svg#dash"/> <use xlink:href="assets/bootstrap-icons.svg#dash"/>
</svg> </svg>
</ng-container> }
<ng-container *ngIf="isExcluded()"> @if (isExcluded()) {
<svg fill="currentColor" class="buttonicon-sm bi-x"> <svg fill="currentColor" class="buttonicon-sm bi-x">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg> </svg>
</ng-container> }
</div> </div>
<div class="me-1"> <div class="me-1">
<pngx-tag *ngIf="isTag; else displayName" [tag]="item" [clickable]="false"></pngx-tag> @if (isTag) {
<ng-template #displayName><small>{{item.name}}</small></ng-template> <pngx-tag [tag]="item" [clickable]="false"></pngx-tag>
} @else {
<small>{{item.name}}</small>
}
</div> </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> </button>

View File

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

View File

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

View File

@ -1,5 +1,7 @@
<div class="mb-3"> <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"> <div class="input-group" [class.is-invalid]="error">
<span class="input-group-text" [style.background-color]="value">&nbsp;&nbsp;&nbsp;</span> <span class="input-group-text" [style.background-color]="value">&nbsp;&nbsp;&nbsp;</span>
@ -21,7 +23,9 @@
</button> </button>
</div> </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"> <div class="invalid-feedback">
{{error}} {{error}}
</div> </div>

View File

@ -2,11 +2,13 @@
<div class="row"> <div class="row">
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal"> <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> <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"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container> </svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button> </button>
}
</div> </div>
<div class="position-relative" [class.col-md-9]="horizontal"> <div class="position-relative" [class.col-md-9]="horizontal">
<div class="input-group" [class.is-invalid]="error"> <div class="input-group" [class.is-invalid]="error">
@ -18,20 +20,26 @@
<use _ngcontent-ng-c3750736003="" xlink:href="assets/bootstrap-icons.svg#calendar"></use> <use _ngcontent-ng-c3750736003="" xlink:href="assets/bootstrap-icons.svg#calendar"></use>
</svg> </svg>
</button> </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"> <svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#filter" /> <use xlink:href="assets/bootstrap-icons.svg#filter" />
</svg> </svg>
</button> </button>
}
</div> </div>
<div class="invalid-feedback position-absolute top-100" i18n>Invalid date.</div> <div class="invalid-feedback position-absolute top-100" i18n>Invalid date.</div>
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small> @if (hint) {
<small *ngIf="getSuggestions().length > 0"> <small class="form-text text-muted">{{hint}}</small>
}
@if (getSuggestions().length > 0) {
<small>
<span i18n>Suggestions:</span>&nbsp; <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; <a (click)="onSuggestionClick(s)" [routerLink]="[]">{{s}}</a>&nbsp;
</ng-container> }
</small> </small>
}
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

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

View File

@ -2,21 +2,27 @@
<div class="row"> <div class="row">
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal"> <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> <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"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container> </svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button> </button>
}
</div> </div>
<div class="position-relative" [class.col-md-9]="horizontal"> <div class="position-relative" [class.col-md-9]="horizontal">
<div class="input-group" [class.is-invalid]="error"> <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"> <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>
<div class="invalid-feedback position-absolute top-100"> <div class="invalid-feedback position-absolute top-100">
{{error}} {{error}}
</div> </div>
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small> @if (hint) {
<small class="form-text text-muted">{{hint}}</small>
}
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,14 +2,18 @@
<label class="form-label" [for]="inputId">{{title}}</label> <label class="form-label" [for]="inputId">{{title}}</label>
<div class="input-group" [class.is-invalid]="error"> <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"> <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"> <svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#eye" /> <use xlink:href="assets/bootstrap-icons.svg#eye" />
</svg> </svg>
</button> </button>
}
</div> </div>
<div class="invalid-feedback"> <div class="invalid-feedback">
{{error}} {{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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@ import {
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
} from '@angular/forms' } from '@angular/forms'
import { SelectComponent } from './select.component' import { SelectComponent } from './select.component'
import { PaperlessTag } from 'src/app/data/paperless-tag' import { Tag } from 'src/app/data/tag'
import { import {
DEFAULT_MATCHING_ALGORITHM, DEFAULT_MATCHING_ALGORITHM,
MATCH_ALL, MATCH_ALL,
@ -18,7 +18,7 @@ import {
import { NgSelectModule } from '@ng-select/ng-select' import { NgSelectModule } from '@ng-select/ng-select'
import { RouterTestingModule } from '@angular/router/testing' import { RouterTestingModule } from '@angular/router/testing'
const items: PaperlessTag[] = [ const items: Tag[] = [
{ {
id: 1, id: 1,
name: 'Tag1', 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) if (name) this.createNew.next(name)
else this.createNew.next(this._lastSearchTerm) else this.createNew.next(this._lastSearchTerm)
this.clearLastSearchTerm() 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"> <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"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg> </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> </span>
</ng-template> </ng-template>
<ng-template ng-option-tmp let-item="item" let-index="index" let-search="searchTerm"> <ng-template ng-option-tmp let-item="item" let-index="index" let-search="searchTerm">
<div class="tag-wrap"> <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> </div>
</ng-template> </ng-template>
</ng-select> </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"> <svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#plus" /> <use xlink:href="assets/bootstrap-icons.svg#plus" />
</svg> </svg>
</button> </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"> <svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#filter" /> <use xlink:href="assets/bootstrap-icons.svg#filter" />
</svg> </svg>
</button> </button>
}
</div> </div>
<small class="form-text text-muted" *ngIf="hint">{{hint}}</small> @if (hint) {
<small *ngIf="getSuggestions().length > 0" class="position-absolute top-100"> <small class="form-text text-muted">{{hint}}</small>
}
@if (getSuggestions().length > 0) {
<small class="position-absolute top-100">
<span i18n>Suggestions:</span>&nbsp; <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; <a (click)="addTag(tag.id)" [routerLink]="[]">{{tag?.name}}</a>&nbsp;
</ng-container> }
</small> </small>
}
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

@ -9,7 +9,7 @@ import {
} from '@angular/core' } from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 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 { TagEditDialogComponent } from '../../edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
import { TagService } from 'src/app/services/rest/tag.service' import { TagService } from 'src/app/services/rest/tag.service'
import { EditDialogMode } from '../../edit-dialog/edit-dialog.component' import { EditDialogMode } from '../../edit-dialog/edit-dialog.component'
@ -81,13 +81,13 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
horizontal: boolean = false horizontal: boolean = false
@Output() @Output()
filterDocuments = new EventEmitter<PaperlessTag[]>() filterDocuments = new EventEmitter<Tag[]>()
@ViewChild('tagSelect') select: NgSelectComponent @ViewChild('tagSelect') select: NgSelectComponent
value: number[] = [] value: number[] = []
tags: PaperlessTag[] = [] tags: Tag[] = []
public createTagRef: (name) => void public createTagRef: (name) => void

View File

@ -2,15 +2,19 @@
<div class="row"> <div class="row">
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal"> <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> <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"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container> </svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button> </button>
}
</div> </div>
<div class="position-relative" [class.col-md-9]="horizontal"> <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"> <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"> <div class="invalid-feedback position-absolute top-100">
{{error}} {{error}}
</div> </div>

View File

@ -2,11 +2,13 @@
<div class="row"> <div class="row">
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal"> <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> <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"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container> </svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button> </button>
}
</div> </div>
<div [class.col-md-9]="horizontal"> <div [class.col-md-9]="horizontal">
<div class="input-group" [class.is-invalid]="error"> <div class="input-group" [class.is-invalid]="error">
@ -20,7 +22,9 @@
{{error}} {{error}}
</div> </div>
</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> </div>
</div> </div>

View File

@ -2,7 +2,9 @@
<div class="col-md text-truncate"> <div class="col-md text-truncate">
<h3 class="text-truncate" style="line-height: 1.4"> <h3 class="text-truncate" style="line-height: 1.4">
{{title}} {{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> </h3>
</div> </div>
<div class="btn-toolbar col col-md-auto"> <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 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 { export enum RenderTextMode {
DISABLED, DISABLED,
ENABLED, ENABLED,

View File

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

View File

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

View File

@ -10,9 +10,11 @@
<div class="list-group list-group-flush"> <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"> <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"> <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"/> <use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg> </svg>
}
</div> </div>
<div class="me-1"> <div class="me-1">
<small i18n>All</small> <small i18n>All</small>
@ -20,9 +22,11 @@
</button> </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"> <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"> <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"/> <use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg> </svg>
}
</div> </div>
<div class="me-1"> <div class="me-1">
<small i18n>My documents</small> <small i18n>My documents</small>
@ -30,19 +34,35 @@
</button> </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"> <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"> <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"/> <use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg> </svg>
}
</div> </div>
<div class="me-1"> <div class="me-1">
<small i18n>Shared with me</small> <small i18n>Shared with me</small>
</div> </div>
</button> </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"> <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"/> <use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg> </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>
<div class="me-1"> <div class="me-1">
<small i18n>Unowned</small> <small i18n>Unowned</small>
@ -50,9 +70,11 @@
</button> </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"> <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"> <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"/> <use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg> </svg>
}
</div> </div>
<div class="me-1 w-100"> <div class="me-1 w-100">
<ng-select <ng-select
@ -71,12 +93,14 @@
</ng-select> </ng-select>
</div> </div>
</button> </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"> <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"> <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> <label class="form-check-label w-100" for="hideUnowned"><small i18n>Hide unowned</small></label>
</div> </div>
</div> </div>
}
</div> </div>
</div> </div>
</div> </div>

View File

@ -145,6 +145,15 @@ describe('PermissionsFilterDropdownComponent', () => {
userID: null, 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) component.setFilter(OwnerFilterType.UNOWNED)
expect(ownerFilterSetResult).toEqual({ expect(ownerFilterSetResult).toEqual({
excludeUsers: [], excludeUsers: [],

View File

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

View File

@ -9,19 +9,23 @@
<div class="col" i18n>Delete</div> <div class="col" i18n>Delete</div>
<div class="col" i18n>View</div> <div class="col" i18n>View</div>
</li> </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-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"> <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"> <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> <label class="form-check-label visually-hidden" for="{{type.key}}_all" i18n>All</label>
</div> </div>
@for (action of PermissionAction | keyvalue; track action) {
<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"> <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}}"> <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> <label class="form-check-label visually-hidden" for="{{type.key}}_{{action.key}}" i18n>{{action.key}}</label>
</div> </div>
}
</li> </li>
<div *ngIf="error" class="invalid-feedback d-block">{{error}}</div> }
@if (error) {
<div class="invalid-feedback d-block">{{error}}</div>
}
</ul> </ul>
</form> </form>

View File

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

View File

@ -5,7 +5,7 @@ import { PdfViewerComponent } from '../pdf-viewer/pdf-viewer.component'
import { By } from '@angular/platform-browser' import { By } from '@angular/platform-browser'
import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe' import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
import { SettingsService } from 'src/app/services/settings.service' 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 { HttpClientTestingModule } from '@angular/common/http/testing'
import { DocumentService } from 'src/app/services/rest/document.service' import { DocumentService } from 'src/app/services/rest/document.service'

View File

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

View File

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

View File

@ -7,26 +7,37 @@
</button> </button>
<div ngbDropdownMenu aria-labelledby="shareLinksDropdown" class="shadow share-links-dropdown"> <div ngbDropdownMenu aria-labelledby="shareLinksDropdown" class="shadow share-links-dropdown">
<ul class="list-group list-group-flush"> <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 No existing links
</li> </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"> <div class="input-group input-group-sm w-100">
<input type="text" class="form-control" aria-label="Share link" [value]="getShareUrl(link)" readonly> <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) }} {{ getDaysRemaining(link) }}
</span> </span>
}
<button type="button" class="btn btn-sm btn-outline-primary" (click)="copy(link)"> <button type="button" class="btn btn-sm btn-outline-primary" (click)="copy(link)">
<svg class="buttonicon" fill="currentColor"> <svg class="buttonicon" fill="currentColor">
<use *ngIf="copied !== link.id" xlink:href="assets/bootstrap-icons.svg#clipboard-fill" /> @if (copied !== link.id) {
<use *ngIf="copied === link.id" xlink:href="assets/bootstrap-icons.svg#clipboard-check-fill" /> <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> </svg><span class="visually-hidden" i18n>Copy</span>
</button> </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"> <svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#box-arrow-up" /> <use xlink:href="assets/bootstrap-icons.svg#box-arrow-up" />
</svg><span class="visually-hidden" i18n>Share</span> </svg><span class="visually-hidden" i18n>Share</span>
</button> </button>
}
<button type="button" class="btn btn-sm btn-outline-danger" (click)="delete(link)"> <button type="button" class="btn btn-sm btn-outline-danger" (click)="delete(link)">
<svg class="buttonicon" fill="currentColor"> <svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" /> <use xlink:href="assets/bootstrap-icons.svg#trash" />
@ -35,6 +46,7 @@
</div> </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> <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>
}
<li class="list-group-item pt-3 pb-2"> <li class="list-group-item pt-3 pb-2">
<div class="input-group input-group-sm w-100"> <div class="input-group input-group-sm w-100">
<div class="form-check form-switch ms-auto small"> <div class="form-check form-switch ms-auto small">
@ -45,13 +57,19 @@
<div class="input-group input-group-sm w-100 mt-2"> <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> <label class="input-group-text" for="addLink"><ng-container i18n>Expires</ng-container>:</label>
<select class="form-select form-select-sm" [(ngModel)]="expirationDays"> <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> </select>
<button class="btn btn-sm btn-outline-primary ms-auto" type="button" (click)="createLink()" [disabled]="loading"> <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> @if (loading) {
<svg *ngIf="!loading" class="buttonicon me-1" fill="currentColor"> <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" /> <use xlink:href="assets/bootstrap-icons.svg#plus" />
</svg> </svg>
}
<ng-container i18n>Create</ng-container> <ng-container i18n>Create</ng-container>
</button> </button>
</div> </div>

View File

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

View File

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

View File

@ -1,9 +1,15 @@
<ng-container *ngIf="tag !== undefined; else privateTag" > @if (tag !== undefined) {
<span *ngIf="!clickable" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</span> @if (!clickable) {
<a [title]="linkTitle" *ngIf="clickable" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</a> <span class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</span>
</ng-container> }
@if (clickable) {
<ng-template #privateTag> <a [title]="linkTitle" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</a>
<span *ngIf="!clickable" class="badge private" i18n>Private</span> }
<a [title]="linkTitle" *ngIf="clickable" class="badge private" i18n>Private</a> } @else {
</ng-template> @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 { ComponentFixture, TestBed } from '@angular/core/testing'
import { TagComponent } from './tag.component' 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' import { By } from '@angular/platform-browser'
const tag: PaperlessTag = { const tag: Tag = {
id: 1, id: 1,
color: '#ff0000', color: '#ff0000',
name: 'Tag1', name: 'Tag1',

View File

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

View File

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

View File

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

View File

@ -9,17 +9,22 @@
[cdkDropListDisabled]="settingsService.globalDropzoneActive" [cdkDropListDisabled]="settingsService.globalDropzoneActive"
(cdkDropListDropped)="onDrop($event)" (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> <div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container> <ng-container i18n>Loading...</ng-container>
</div> </div>
}
<div *ngIf="settingsService.offerTour()" class="col"> @if (settingsService.offerTour()) {
<div class="col">
<pngx-welcome-widget (dismiss)="completeTour()"></pngx-welcome-widget> <pngx-welcome-widget (dismiss)="completeTour()"></pngx-welcome-widget>
</div> </div>
}
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }"> <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 <pngx-saved-view-widget
[savedView]="v" [savedView]="v"
(cdkDragStarted)="onDragStart($event)" (cdkDragStarted)="onDragStart($event)"
@ -27,6 +32,7 @@
> >
</pngx-saved-view-widget> </pngx-saved-view-widget>
</div> </div>
}
</ng-container> </ng-container>
</div> </div>
</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 { LogoComponent } from '../common/logo/logo.component'
import { of, throwError } from 'rxjs' import { of, throwError } from 'rxjs'
import { ToastService } from 'src/app/services/toast.service' 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 { 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 = [ const saved_views = [
{ {
@ -167,7 +167,7 @@ describe('DashboardComponent', () => {
const toastSpy = jest.spyOn(toastService, 'showInfo') const toastSpy = jest.spyOn(toastService, 'showInfo')
jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true)) jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true))
component.onDrop({ previousIndex: 0, currentIndex: 1 } as CdkDragDrop< component.onDrop({ previousIndex: 0, currentIndex: 1 } as CdkDragDrop<
PaperlessSavedView[] SavedView[]
>) >)
expect(settingsSpy).toHaveBeenCalledWith([ expect(settingsSpy).toHaveBeenCalledWith([
saved_views[2], saved_views[2],
@ -190,7 +190,7 @@ describe('DashboardComponent', () => {
.spyOn(settingsService, 'storeSettings') .spyOn(settingsService, 'storeSettings')
.mockReturnValue(throwError(() => new Error('unable to save'))) .mockReturnValue(throwError(() => new Error('unable to save')))
component.onDrop({ previousIndex: 0, currentIndex: 2 } as CdkDragDrop< component.onDrop({ previousIndex: 0, currentIndex: 2 } as CdkDragDrop<
PaperlessSavedView[] SavedView[]
>) >)
expect(toastSpy).toHaveBeenCalled() 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 { SettingsService } from 'src/app/services/settings.service'
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component' import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
import { TourService } from 'ngx-ui-tour-ng-bootstrap' 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 { 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 { import {
CdkDragDrop, CdkDragDrop,
CdkDragEnd, CdkDragEnd,
@ -19,7 +19,7 @@ import {
styleUrls: ['./dashboard.component.scss'], styleUrls: ['./dashboard.component.scss'],
}) })
export class DashboardComponent extends ComponentWithPermissions { export class DashboardComponent extends ComponentWithPermissions {
public dashboardViews: PaperlessSavedView[] = [] public dashboardViews: SavedView[] = []
constructor( constructor(
public settingsService: SettingsService, public settingsService: SettingsService,
public savedViewService: SavedViewService, public savedViewService: SavedViewService,
@ -57,7 +57,7 @@ export class DashboardComponent extends ComponentWithPermissions {
this.settingsService.globalDropzoneEnabled = true this.settingsService.globalDropzoneEnabled = true
} }
onDrop(event: CdkDragDrop<PaperlessSavedView[]>) { onDrop(event: CdkDragDrop<SavedView[]>) {
moveItemInArray( moveItemInArray(
this.dashboardViews, this.dashboardViews,
event.previousIndex, event.previousIndex,

View File

@ -5,9 +5,12 @@
[draggable]="savedView" [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> <thead>
<tr> <tr>
<th scope="col" i18n>Created</th> <th scope="col" i18n>Created</th>
@ -17,16 +20,21 @@
</tr> </tr>
</thead> </thead>
<tbody> <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}}" 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"> <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> <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>
<td class="py-2 py-md-3 d-none d-md-table-cell"> <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>
<td class="position-relative py-2 py-md-3 d-none d-md-table-cell"> <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"> <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" <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" [ngbPopover]="previewContent" [popoverTitle]="doc.title | documentTitle"
@ -46,11 +54,12 @@
</div> </div>
</td> </td>
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
} @else {
<ng-template #empty>
<p i18n class="text-center text-muted mb-0 fst-italic">No documents</p> <p i18n class="text-center text-muted mb-0 fst-italic">No documents</p>
</ng-template> }
</pngx-widget-frame> </pngx-widget-frame>

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