mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-02 13:45:10 -05:00
Merge branch 'dev'
This commit is contained in:
commit
9c8b43f602
@ -1,4 +1,4 @@
|
|||||||
bind = '127.0.0.1:8000'
|
bind = ['[::]:8000', 'localhost:8000']
|
||||||
backlog = 2048
|
backlog = 2048
|
||||||
workers = 3
|
workers = 3
|
||||||
worker_class = 'sync'
|
worker_class = 'sync'
|
||||||
|
@ -15,7 +15,7 @@ services:
|
|||||||
POSTGRES_PASSWORD: paperless
|
POSTGRES_PASSWORD: paperless
|
||||||
|
|
||||||
webserver:
|
webserver:
|
||||||
image: jonaswinkler/paperless-ng:0.9.9
|
image: jonaswinkler/paperless-ng:0.9.10
|
||||||
restart: always
|
restart: always
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
|
@ -5,7 +5,7 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
webserver:
|
webserver:
|
||||||
image: jonaswinkler/paperless-ng:0.9.9
|
image: jonaswinkler/paperless-ng:0.9.10
|
||||||
restart: always
|
restart: always
|
||||||
depends_on:
|
depends_on:
|
||||||
- broker
|
- broker
|
||||||
|
@ -9,6 +9,8 @@ RUN apt-get update \
|
|||||||
&& apt-get -y --no-install-recommends install \
|
&& apt-get -y --no-install-recommends install \
|
||||||
build-essential \
|
build-essential \
|
||||||
curl \
|
curl \
|
||||||
|
file \
|
||||||
|
fonts-liberation \
|
||||||
ghostscript \
|
ghostscript \
|
||||||
gnupg \
|
gnupg \
|
||||||
icc-profiles-free \
|
icc-profiles-free \
|
||||||
@ -20,6 +22,7 @@ RUN apt-get update \
|
|||||||
libpq-dev \
|
libpq-dev \
|
||||||
libqpdf-dev \
|
libqpdf-dev \
|
||||||
libxml2 \
|
libxml2 \
|
||||||
|
libxslt1-dev \
|
||||||
optipng \
|
optipng \
|
||||||
pngquant \
|
pngquant \
|
||||||
qpdf \
|
qpdf \
|
||||||
@ -62,6 +65,7 @@ RUN sudo -HEu paperless python3 manage.py collectstatic --clear --no-input
|
|||||||
|
|
||||||
VOLUME ["/usr/src/paperless/data", "/usr/src/paperless/media", "/usr/src/paperless/consume", "/usr/src/paperless/export"]
|
VOLUME ["/usr/src/paperless/data", "/usr/src/paperless/media", "/usr/src/paperless/consume", "/usr/src/paperless/export"]
|
||||||
ENTRYPOINT ["/sbin/docker-entrypoint.sh"]
|
ENTRYPOINT ["/sbin/docker-entrypoint.sh"]
|
||||||
|
EXPOSE 8000
|
||||||
CMD ["/usr/local/bin/supervisord", "-c", "/etc/supervisord.conf"]
|
CMD ["/usr/local/bin/supervisord", "-c", "/etc/supervisord.conf"]
|
||||||
|
|
||||||
LABEL maintainer="Jonas Winkler <dev@jpwinkler.de>"
|
LABEL maintainer="Jonas Winkler <dev@jpwinkler.de>"
|
||||||
|
@ -8,7 +8,7 @@ loglevel=info ; log level; default info; others: debug,warn,trace
|
|||||||
user=root
|
user=root
|
||||||
|
|
||||||
[program:gunicorn]
|
[program:gunicorn]
|
||||||
command=gunicorn -c /usr/src/paperless/gunicorn.conf.py -b 0.0.0.0:8000 paperless.wsgi
|
command=gunicorn -c /usr/src/paperless/gunicorn.conf.py paperless.wsgi
|
||||||
user=paperless
|
user=paperless
|
||||||
|
|
||||||
stdout_logfile=/dev/stdout
|
stdout_logfile=/dev/stdout
|
||||||
|
@ -5,6 +5,37 @@
|
|||||||
Changelog
|
Changelog
|
||||||
*********
|
*********
|
||||||
|
|
||||||
|
paperless-ng 0.9.10
|
||||||
|
###################
|
||||||
|
|
||||||
|
* Bulk editing
|
||||||
|
|
||||||
|
* Thanks to `Michael Shamoon`_, we've got a new interface for the bulk editor.
|
||||||
|
* There are some configuration options in the settings to alter the behavior.
|
||||||
|
|
||||||
|
* Other changes and additions
|
||||||
|
|
||||||
|
* The Paperless-ng logo now navigates to the dashboard.
|
||||||
|
* Filter for documents that don't have any correspondents, types or tags assigned.
|
||||||
|
* Tags, types and correspondents are now sorted case insensitive.
|
||||||
|
* Lots of preparation work for localization support.
|
||||||
|
|
||||||
|
* Fixes
|
||||||
|
|
||||||
|
* Added missing dependencies for Raspberry Pi builds.
|
||||||
|
* Fixed an issue with plain text file consumption: Thumbnail generation failed due to missing fonts.
|
||||||
|
* An issue with the search index reporting missing documents after bulk deletes was fixed.
|
||||||
|
* Issue with the tag selector not clearing input correctly.
|
||||||
|
* The consumer used to stop working when encountering an incomplete classifier model file.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The bulk delete operations did not update the search index. Therefore, documents that you deleted remained in the index and
|
||||||
|
caused the search to return messages about missing documents when searching. Further bulk operations will properly update
|
||||||
|
the index.
|
||||||
|
|
||||||
|
However, this change is not retroactive: If you used the delete method of the bulk editor, you need to reindex your search index
|
||||||
|
by :ref:`running the management command document_index with the argument reindex <administration-index>`.
|
||||||
|
|
||||||
paperless-ng 0.9.9
|
paperless-ng 0.9.9
|
||||||
##################
|
##################
|
||||||
|
@ -400,6 +400,15 @@ PAPERLESS_FILENAME_DATE_ORDER=<format>
|
|||||||
|
|
||||||
Defaults to none, which disables this feature.
|
Defaults to none, which disables this feature.
|
||||||
|
|
||||||
|
PAPERLESS_THUMBNAIL_FONT_NAME=<filename>
|
||||||
|
Paperless creates thumbnails for plain text files by rendering the content
|
||||||
|
of the file on an image and uses a predefined font for that. This
|
||||||
|
font can be changed here.
|
||||||
|
|
||||||
|
Note that this won't have any effect on already generated thumbnails.
|
||||||
|
|
||||||
|
Defaults to ``/usr/share/fonts/liberation/LiberationSerif-Regular.ttf``.
|
||||||
|
|
||||||
|
|
||||||
Binaries
|
Binaries
|
||||||
########
|
########
|
||||||
|
@ -221,8 +221,9 @@ writing. Windows is not and will never be supported.
|
|||||||
* ``python3-pip``, optionally ``pipenv`` for package installation
|
* ``python3-pip``, optionally ``pipenv`` for package installation
|
||||||
* ``python3-dev``
|
* ``python3-dev``
|
||||||
|
|
||||||
|
* ``fonts-liberation`` for generating thumbnails for plain text files
|
||||||
* ``imagemagick`` >= 6 for PDF conversion
|
* ``imagemagick`` >= 6 for PDF conversion
|
||||||
* ``optipng`` for optimising thumbnails
|
* ``optipng`` for optimizing thumbnails
|
||||||
* ``gnupg`` for handling encrypted documents
|
* ``gnupg`` for handling encrypted documents
|
||||||
* ``libpoppler-cpp-dev`` for PDF to text conversion
|
* ``libpoppler-cpp-dev`` for PDF to text conversion
|
||||||
* ``libmagic-dev`` for mime type detection
|
* ``libmagic-dev`` for mime type detection
|
||||||
@ -242,8 +243,7 @@ writing. Windows is not and will never be supported.
|
|||||||
* ``tesseract-ocr`` language packs (``tesseract-ocr-eng``, ``tesseract-ocr-deu``, etc)
|
* ``tesseract-ocr`` language packs (``tesseract-ocr-eng``, ``tesseract-ocr-deu``, etc)
|
||||||
|
|
||||||
You will also need ``build-essential``, ``python3-setuptools`` and ``python3-wheel``
|
You will also need ``build-essential``, ``python3-setuptools`` and ``python3-wheel``
|
||||||
for installing some of the python dependencies. You can remove that
|
for installing some of the python dependencies.
|
||||||
again after installation.
|
|
||||||
|
|
||||||
2. Install ``redis`` >= 5.0 and configure it to start automatically.
|
2. Install ``redis`` >= 5.0 and configure it to start automatically.
|
||||||
|
|
||||||
|
@ -54,6 +54,7 @@
|
|||||||
#PAPERLESS_POST_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh
|
#PAPERLESS_POST_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh
|
||||||
#PAPERLESS_FILENAME_DATE_ORDER=YMD
|
#PAPERLESS_FILENAME_DATE_ORDER=YMD
|
||||||
#PAPERLESS_FILENAME_PARSE_TRANSFORMS=[]
|
#PAPERLESS_FILENAME_PARSE_TRANSFORMS=[]
|
||||||
|
#PAPERLESS_THUMBNAIL_FONT_NAME=
|
||||||
|
|
||||||
# Binaries
|
# Binaries
|
||||||
|
|
||||||
|
@ -26,12 +26,16 @@
|
|||||||
"aot": true,
|
"aot": true,
|
||||||
"assets": [
|
"assets": [
|
||||||
"src/favicon.ico",
|
"src/favicon.ico",
|
||||||
"src/assets"
|
"src/assets",
|
||||||
|
"src/manifest.webmanifest"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.scss"
|
"src/styles.scss"
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": [],
|
||||||
|
"allowedCommonJsDependencies": [
|
||||||
|
"ng2-pdf-viewer"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
@ -90,7 +94,8 @@
|
|||||||
"karmaConfig": "karma.conf.js",
|
"karmaConfig": "karma.conf.js",
|
||||||
"assets": [
|
"assets": [
|
||||||
"src/favicon.ico",
|
"src/favicon.ico",
|
||||||
"src/assets"
|
"src/assets",
|
||||||
|
"src/manifest.webmanifest"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.scss"
|
"src/styles.scss"
|
||||||
@ -127,4 +132,4 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultProject": "paperless-ui"
|
"defaultProject": "paperless-ui"
|
||||||
}
|
}
|
||||||
|
1608
src-ui/messages.xlf
Normal file
1608
src-ui/messages.xlf
Normal file
File diff suppressed because it is too large
Load Diff
@ -26,12 +26,13 @@ import { ResultHighlightComponent } from './components/search/result-highlight/r
|
|||||||
import { PageHeaderComponent } from './components/common/page-header/page-header.component';
|
import { PageHeaderComponent } from './components/common/page-header/page-header.component';
|
||||||
import { AppFrameComponent } from './components/app-frame/app-frame.component';
|
import { AppFrameComponent } from './components/app-frame/app-frame.component';
|
||||||
import { ToastsComponent } from './components/common/toasts/toasts.component';
|
import { ToastsComponent } from './components/common/toasts/toasts.component';
|
||||||
import { FilterEditorComponent } from './components/filter-editor/filter-editor.component';
|
import { FilterEditorComponent } from './components/document-list/filter-editor/filter-editor.component';
|
||||||
import { FilterDropdownComponent } from './components/filter-editor/filter-dropdown/filter-dropdown.component';
|
import { FilterableDropdownComponent } from './components/common/filterable-dropdown/filterable-dropdown.component';
|
||||||
import { FilterDropdownButtonComponent } from './components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component';
|
import { ToggleableDropdownButtonComponent } from './components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component';
|
||||||
import { FilterDropdownDateComponent } from './components/filter-editor/filter-dropdown-date/filter-dropdown-date.component';
|
import { DateDropdownComponent } from './components/common/date-dropdown/date-dropdown.component';
|
||||||
import { DocumentCardLargeComponent } from './components/document-list/document-card-large/document-card-large.component';
|
import { DocumentCardLargeComponent } from './components/document-list/document-card-large/document-card-large.component';
|
||||||
import { DocumentCardSmallComponent } from './components/document-list/document-card-small/document-card-small.component';
|
import { DocumentCardSmallComponent } from './components/document-list/document-card-small/document-card-small.component';
|
||||||
|
import { BulkEditorComponent } from './components/document-list/bulk-editor/bulk-editor.component';
|
||||||
import { NgxFileDropModule } from 'ngx-file-drop';
|
import { NgxFileDropModule } from 'ngx-file-drop';
|
||||||
import { TextComponent } from './components/common/input/text/text.component';
|
import { TextComponent } from './components/common/input/text/text.component';
|
||||||
import { SelectComponent } from './components/common/input/select/select.component';
|
import { SelectComponent } from './components/common/input/select/select.component';
|
||||||
@ -54,8 +55,8 @@ import { FileSizePipe } from './pipes/file-size.pipe';
|
|||||||
import { FilterPipe } from './pipes/filter.pipe';
|
import { FilterPipe } from './pipes/filter.pipe';
|
||||||
import { DocumentTitlePipe } from './pipes/document-title.pipe';
|
import { DocumentTitlePipe } from './pipes/document-title.pipe';
|
||||||
import { MetadataCollapseComponent } from './components/document-detail/metadata-collapse/metadata-collapse.component';
|
import { MetadataCollapseComponent } from './components/document-detail/metadata-collapse/metadata-collapse.component';
|
||||||
import { NgSelectModule } from '@ng-select/ng-select';
|
|
||||||
import { SelectDialogComponent } from './components/common/select-dialog/select-dialog.component';
|
import { SelectDialogComponent } from './components/common/select-dialog/select-dialog.component';
|
||||||
|
import { NgSelectModule } from '@ng-select/ng-select';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@ -80,11 +81,12 @@ import { SelectDialogComponent } from './components/common/select-dialog/select-
|
|||||||
AppFrameComponent,
|
AppFrameComponent,
|
||||||
ToastsComponent,
|
ToastsComponent,
|
||||||
FilterEditorComponent,
|
FilterEditorComponent,
|
||||||
FilterDropdownComponent,
|
FilterableDropdownComponent,
|
||||||
FilterDropdownButtonComponent,
|
ToggleableDropdownButtonComponent,
|
||||||
FilterDropdownDateComponent,
|
DateDropdownComponent,
|
||||||
DocumentCardLargeComponent,
|
DocumentCardLargeComponent,
|
||||||
DocumentCardSmallComponent,
|
DocumentCardSmallComponent,
|
||||||
|
BulkEditorComponent,
|
||||||
TextComponent,
|
TextComponent,
|
||||||
SelectComponent,
|
SelectComponent,
|
||||||
CheckComponent,
|
CheckComponent,
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
<nav class="navbar navbar-dark sticky-top bg-primary flex-md-nowrap p-0 shadow">
|
<nav class="navbar navbar-dark sticky-top bg-primary flex-md-nowrap p-0 shadow">
|
||||||
<span class="navbar-brand col-md-3 col-lg-2 mr-0 px-3" href="#">
|
<a class="navbar-brand col-md-3 col-lg-2 mr-0 px-3" routerLink="/dashboard">
|
||||||
<img src="assets/logo-dark-notext.svg" height="18px" class="mr-2">
|
<img src="assets/logo-dark-notext.svg" height="18px" class="mr-2">
|
||||||
Paperless-ng
|
<ng-container i18n="app title">Paperless-ng</ng-container>
|
||||||
</span>
|
</a>
|
||||||
<button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-toggle="collapse"
|
<button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-toggle="collapse"
|
||||||
data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation"
|
data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation"
|
||||||
(click)="isMenuCollapsed = !isMenuCollapsed">
|
(click)="isMenuCollapsed = !isMenuCollapsed">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<form (ngSubmit)="search()" class="w-100 m-1">
|
<form (ngSubmit)="search()" class="w-100 m-1">
|
||||||
<input class="form-control form-control-dark" type="text" placeholder="Search" aria-label="Search"
|
<input class="form-control form-control-dark" type="text" placeholder="Search for documents" aria-label="Search"
|
||||||
[formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (selectItem)="itemSelected($event)">
|
[formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (selectItem)="itemSelected($event)" i18n-placeholder>
|
||||||
</form>
|
</form>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@ -28,136 +28,122 @@
|
|||||||
<a class="nav-link" routerLink="dashboard" routerLinkActive="active" (click)="closeMenu()">
|
<a class="nav-link" routerLink="dashboard" routerLinkActive="active" (click)="closeMenu()">
|
||||||
<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>
|
</svg> <ng-container i18n>Dashboard</ng-container>
|
||||||
Dashboard
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="documents" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" (click)="closeMenu()">
|
<a class="nav-link" routerLink="documents" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" (click)="closeMenu()">
|
||||||
<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>
|
</svg> <ng-container i18n>Documents</ng-container>
|
||||||
Documents
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted" *ngIf='savedViewService.sidebarViews.length > 0'>
|
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted" *ngIf='savedViewService.sidebarViews.length > 0'>
|
||||||
<span>Saved views</span>
|
<ng-container i18n>Saved views</ng-container>
|
||||||
</h6>
|
</h6>
|
||||||
<ul class="nav flex-column mb-2">
|
<ul class="nav flex-column mb-2">
|
||||||
<li class="nav-item w-100" *ngFor="let view of savedViewService.sidebarViews">
|
<li class="nav-item w-100" *ngFor="let view of savedViewService.sidebarViews">
|
||||||
<a class="nav-link text-truncate" routerLink="view/{{view.id}}" routerLinkActive="active" (click)="closeMenu()">
|
<a class="nav-link text-truncate" routerLink="view/{{view.id}}" routerLinkActive="active" (click)="closeMenu()">
|
||||||
<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>
|
</svg> {{view.name}}
|
||||||
{{view.name}}
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted" *ngIf='openDocuments.length > 0'>
|
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted" *ngIf='openDocuments.length > 0'>
|
||||||
<span>Open documents</span>
|
<ng-container i18n>Open documents</ng-container>
|
||||||
</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'>
|
<li class="nav-item w-100" *ngFor='let d of openDocuments'>
|
||||||
<a class="nav-link text-truncate" routerLink="documents/{{d.id}}" routerLinkActive="active" (click)="closeMenu()">
|
<a class="nav-link text-truncate" routerLink="documents/{{d.id}}" routerLinkActive="active" (click)="closeMenu()">
|
||||||
<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>
|
</svg> {{d.title | documentTitle}}
|
||||||
{{d.title | documentTitle}}
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item w-100" *ngIf="openDocuments.length > 1">
|
<li class="nav-item w-100" *ngIf="openDocuments.length > 1">
|
||||||
<a class="nav-link text-truncate" [routerLink]="" (click)="closeAll()">
|
<a class="nav-link text-truncate" [routerLink]="" (click)="closeAll()">
|
||||||
<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>
|
</svg> <ng-container i18n>Close all</ng-container>
|
||||||
Close all
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||||
<span>Manage</span>
|
<ng-container i18n>Manage</ng-container>
|
||||||
</h6>
|
</h6>
|
||||||
<ul class="nav flex-column mb-2">
|
<ul class="nav flex-column mb-2">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()">
|
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()">
|
||||||
<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>
|
</svg> <ng-container i18n>Correspondents</ng-container>
|
||||||
Correspondents
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()">
|
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()">
|
||||||
<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>
|
</svg> <ng-container i18n>Tags</ng-container>
|
||||||
Tags
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()">
|
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()">
|
||||||
<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>
|
</svg> <ng-container i18n>Document types</ng-container>
|
||||||
Document types
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()">
|
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()">
|
||||||
<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>
|
</svg> <ng-container i18n>Logs</ng-container>
|
||||||
Logs
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()">
|
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()">
|
||||||
<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>
|
</svg> <ng-container i18n>Settings</ng-container>
|
||||||
Settings
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="admin/">
|
<a class="nav-link" href="admin/">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#toggles"/>
|
<use xlink:href="assets/bootstrap-icons.svg#toggles"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>Admin</ng-container>
|
||||||
Admin
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||||
<span>Misc</span>
|
<ng-container i18n>Misc</ng-container>
|
||||||
</h6>
|
</h6>
|
||||||
<ul class="nav flex-column mb-2">
|
<ul class="nav flex-column mb-2">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" target="_blank" rel="noopener noreferrer" href="https://paperless-ng.readthedocs.io/en/latest/">
|
<a class="nav-link" target="_blank" rel="noopener noreferrer" href="https://paperless-ng.readthedocs.io/en/latest/">
|
||||||
<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>
|
</svg> <ng-container i18n>Documentation</ng-container>
|
||||||
Documentation
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" target="_blank" rel="noopener noreferrer" href="https://github.com/jonaswinkler/paperless-ng">
|
<a class="nav-link" target="_blank" rel="noopener noreferrer" href="https://github.com/jonaswinkler/paperless-ng">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#link"/>
|
<use xlink:href="assets/bootstrap-icons.svg#link"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>GitHub</ng-container>
|
||||||
GitHub
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="accounts/logout/">
|
<a class="nav-link" href="accounts/logout/">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#door-open"/>
|
<use xlink:href="assets/bootstrap-icons.svg#door-open"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>Logout</ng-container>
|
||||||
Logout
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -14,7 +14,7 @@ export class ConfirmDialogComponent implements OnInit {
|
|||||||
public confirmClicked = new EventEmitter()
|
public confirmClicked = new EventEmitter()
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
title = "Confirmation"
|
title = $localize`Confirmation`
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
messageBold
|
messageBold
|
||||||
@ -26,7 +26,7 @@ export class ConfirmDialogComponent implements OnInit {
|
|||||||
btnClass = "btn-primary"
|
btnClass = "btn-primary"
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
btnCaption = "Confirm"
|
btnCaption = $localize`Confirm`
|
||||||
|
|
||||||
confirmButtonEnabled = true
|
confirmButtonEnabled = true
|
||||||
seconds = 0
|
seconds = 0
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="dateBefore || dateAfter ? 'btn-primary' : 'btn-outline-primary'">
|
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="dateBefore || dateAfter ? 'btn-primary' : 'btn-outline-primary'">
|
||||||
{{title}}
|
{{title}}
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-menu date-filter 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 qf of quickFilters" class="list-group-item small list-goup list-group-item-action d-flex p-2 pl-3" role="menuitem" (click)="setDateQuickFilter(qf.id)">
|
<button *ngFor="let qf of quickFilters" class="list-group-item small list-goup list-group-item-action d-flex p-2 pl-3" role="menuitem" (click)="setDateQuickFilter(qf.id)">
|
||||||
{{qf.name}}
|
{{qf.name}}
|
||||||
@ -10,12 +10,12 @@
|
|||||||
<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>After</div>
|
<div i18n>After</div>
|
||||||
<a *ngIf="dateAfter" class="btn btn-link p-0 m-0" (click)="clearAfter()">
|
<a *ngIf="dateAfter" class="btn btn-link p-0 m-0" (click)="clearAfter()">
|
||||||
<svg width="0.8em" height="0.8em" viewBox="0 0 16 16" class="bi bi-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
<svg width="0.8em" height="0.8em" viewBox="0 0 16 16" class="bi bi-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
|
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
|
||||||
</svg>
|
</svg>
|
||||||
<small>Clear</small>
|
<small i18n>Clear</small>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -26,12 +26,12 @@
|
|||||||
<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>Before</div>
|
<div i18n>Before</div>
|
||||||
<a *ngIf="dateBefore" class="btn btn-link p-0 m-0" (click)="clearBefore()">
|
<a *ngIf="dateBefore" class="btn btn-link p-0 m-0" (click)="clearBefore()">
|
||||||
<svg width="0.8em" height="0.8em" viewBox="0 0 16 16" class="bi bi-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
<svg width="0.8em" height="0.8em" viewBox="0 0 16 16" class="bi bi-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
|
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
|
||||||
</svg>
|
</svg>
|
||||||
<small>Clear</small>
|
<small i18n>Clear</small>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
.date-filter {
|
.date-dropdown {
|
||||||
min-width: 250px;
|
min-width: 250px;
|
||||||
|
|
||||||
.btn-link {
|
.btn-link {
|
@ -1,20 +1,20 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { FilterDropodownComponent } from './filter-dropdown.component';
|
import { DateDropdownComponent } from './date-dropdown.component';
|
||||||
|
|
||||||
describe('FilterDropodownComponent', () => {
|
describe('DateDropdownComponent', () => {
|
||||||
let component: FilterDropodownComponent;
|
let component: DateDropdownComponent;
|
||||||
let fixture: ComponentFixture<FilterDropodownComponent>;
|
let fixture: ComponentFixture<DateDropdownComponent>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [ FilterDropodownComponent ]
|
declarations: [ DateDropdownComponent ]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(FilterDropodownComponent);
|
fixture = TestBed.createComponent(DateDropdownComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
@ -8,31 +8,37 @@ export interface DateSelection {
|
|||||||
after?: string
|
after?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const FILTER_LAST_7_DAYS = 0
|
const LAST_7_DAYS = 0
|
||||||
const FILTER_LAST_MONTH = 1
|
const LAST_MONTH = 1
|
||||||
const FILTER_LAST_3_MONTHS = 2
|
const LAST_3_MONTHS = 2
|
||||||
const FILTER_LAST_YEAR = 3
|
const LAST_YEAR = 3
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-filter-dropdown-date',
|
selector: 'app-date-dropdown',
|
||||||
templateUrl: './filter-dropdown-date.component.html',
|
templateUrl: './date-dropdown.component.html',
|
||||||
styleUrls: ['./filter-dropdown-date.component.scss']
|
styleUrls: ['./date-dropdown.component.scss']
|
||||||
})
|
})
|
||||||
export class FilterDropdownDateComponent implements OnInit, OnDestroy {
|
export class DateDropdownComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
quickFilters = [
|
quickFilters = [
|
||||||
{id: FILTER_LAST_7_DAYS, name: "Last 7 days"},
|
{id: LAST_7_DAYS, name: $localize`Last 7 days`},
|
||||||
{id: FILTER_LAST_MONTH, name: "Last month"},
|
{id: LAST_MONTH, name: $localize`Last month`},
|
||||||
{id: FILTER_LAST_3_MONTHS, name: "Last 3 months"},
|
{id: LAST_3_MONTHS, name: $localize`Last 3 months`},
|
||||||
{id: FILTER_LAST_YEAR, name: "Last year"}
|
{id: LAST_YEAR, name: $localize`Last year`}
|
||||||
]
|
]
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
dateBefore: string
|
dateBefore: string
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
dateBeforeChange = new EventEmitter<string>()
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
dateAfter: string
|
dateAfter: string
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
dateAfterChange = new EventEmitter<string>()
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
title: string
|
title: string
|
||||||
|
|
||||||
@ -42,7 +48,7 @@ export class FilterDropdownDateComponent implements OnInit, OnDestroy {
|
|||||||
private datesSetDebounce$ = new Subject()
|
private datesSetDebounce$ = new Subject()
|
||||||
|
|
||||||
private sub: Subscription
|
private sub: Subscription
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.sub = this.datesSetDebounce$.pipe(
|
this.sub = this.datesSetDebounce$.pipe(
|
||||||
debounceTime(400)
|
debounceTime(400)
|
||||||
@ -61,28 +67,30 @@ export class FilterDropdownDateComponent implements OnInit, OnDestroy {
|
|||||||
this.dateBefore = null
|
this.dateBefore = null
|
||||||
let date = new Date()
|
let date = new Date()
|
||||||
switch (qf) {
|
switch (qf) {
|
||||||
case FILTER_LAST_7_DAYS:
|
case LAST_7_DAYS:
|
||||||
date.setDate(date.getDate() - 7)
|
date.setDate(date.getDate() - 7)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case FILTER_LAST_MONTH:
|
case LAST_MONTH:
|
||||||
date.setMonth(date.getMonth() - 1)
|
date.setMonth(date.getMonth() - 1)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case FILTER_LAST_3_MONTHS:
|
case LAST_3_MONTHS:
|
||||||
date.setMonth(date.getMonth() - 3)
|
date.setMonth(date.getMonth() - 3)
|
||||||
break
|
break
|
||||||
|
|
||||||
case FILTER_LAST_YEAR:
|
case LAST_YEAR:
|
||||||
date.setFullYear(date.getFullYear() - 1)
|
date.setFullYear(date.getFullYear() - 1)
|
||||||
break
|
break
|
||||||
|
|
||||||
}
|
}
|
||||||
this.dateAfter = formatDate(date, 'yyyy-MM-dd', "en-us", "UTC")
|
this.dateAfter = formatDate(date, 'yyyy-MM-dd', "en-us", "UTC")
|
||||||
this.onChange()
|
this.onChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange() {
|
onChange() {
|
||||||
|
this.dateAfterChange.emit(this.dateAfter)
|
||||||
|
this.dateBeforeChange.emit(this.dateBefore)
|
||||||
this.datesSet.emit({after: this.dateAfter, before: this.dateBefore})
|
this.datesSet.emit({after: this.dateAfter, before: this.dateBefore})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,12 +99,12 @@ export class FilterDropdownDateComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearBefore() {
|
clearBefore() {
|
||||||
this.dateBefore = null;
|
this.dateBefore = null
|
||||||
this.onChange()
|
this.onChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
clearAfter() {
|
clearAfter() {
|
||||||
this.dateAfter = null;
|
this.dateAfter = null
|
||||||
this.onChange()
|
this.onChange()
|
||||||
}
|
}
|
||||||
|
|
@ -5,7 +5,7 @@ import { Observable } from 'rxjs';
|
|||||||
import { MATCHING_ALGORITHMS } from 'src/app/data/matching-model';
|
import { MATCHING_ALGORITHMS } 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 { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service';
|
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service';
|
||||||
import { Toast, ToastService } from 'src/app/services/toast.service';
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export abstract class EditDialogComponent<T extends ObjectWithId> implements OnInit {
|
export abstract class EditDialogComponent<T extends ObjectWithId> implements OnInit {
|
||||||
@ -13,8 +13,7 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI
|
|||||||
constructor(
|
constructor(
|
||||||
private service: AbstractPaperlessService<T>,
|
private service: AbstractPaperlessService<T>,
|
||||||
private activeModal: NgbActiveModal,
|
private activeModal: NgbActiveModal,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService) { }
|
||||||
private entityName: string) { }
|
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
dialogMode: string = 'create'
|
dialogMode: string = 'create'
|
||||||
@ -35,12 +34,24 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCreateTitle() {
|
||||||
|
return $localize`Create new item`
|
||||||
|
}
|
||||||
|
|
||||||
|
getEditTitle() {
|
||||||
|
return $localize`Edit item`
|
||||||
|
}
|
||||||
|
|
||||||
|
getSaveErrorMessage(error: string) {
|
||||||
|
return $localize`Could not save element: ${error}`
|
||||||
|
}
|
||||||
|
|
||||||
getTitle() {
|
getTitle() {
|
||||||
switch (this.dialogMode) {
|
switch (this.dialogMode) {
|
||||||
case 'create':
|
case 'create':
|
||||||
return "Create new " + this.entityName
|
return this.getCreateTitle()
|
||||||
case 'edit':
|
case 'edit':
|
||||||
return "Edit " + this.entityName
|
return this.getEditTitle()
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -66,7 +77,7 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI
|
|||||||
this.activeModal.close()
|
this.activeModal.close()
|
||||||
this.success.emit(result)
|
this.success.emit(result)
|
||||||
}, error => {
|
}, error => {
|
||||||
this.toastService.showToast(Toast.makeError(`Could not save ${this.entityName}: ${error.error.name}`))
|
this.toastService.showError(this.getSaveErrorMessage(error.error.name))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
<div class="btn-group" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown">
|
||||||
|
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'">
|
||||||
|
<div class="d-none d-md-inline">{{title}}</div>
|
||||||
|
<div class="d-inline-block d-md-none">
|
||||||
|
<svg class="toolbaricon" fill="currentColor">
|
||||||
|
<use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<ng-container *ngIf="!editing && selectionModel.selectionSize() > 0">
|
||||||
|
<div class="badge bg-secondary text-light rounded-pill badge-corner">
|
||||||
|
{{selectionModel.selectionSize()}}
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<input class="form-control" type="text" [(ngModel)]="filterText" [placeholder]="filterPlaceholder" (keyup.enter)="listFilterEnter()" #listFilterTextInput>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="selectionModel.items" class="items">
|
||||||
|
<ng-container *ngFor="let item of (editing ? selectionModel.itemsSorted : selectionModel.items) | filter: filterText">
|
||||||
|
<app-toggleable-dropdown-button *ngIf="allowSelectNone || item.id" [item]="item" [state]="selectionModel.get(item.id)" (toggle)="selectionModel.toggle(item.id)"></app-toggleable-dropdown-button>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
<button *ngIf="editing" class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!selectionModel.isDirty()">
|
||||||
|
<small class="ml-1" [ngClass]="{'font-weight-bold': selectionModel.isDirty()}" i18n>Apply</small>
|
||||||
|
<svg width="1.5em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#arrow-right" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FilterableDropodownComponent } from './filterable-dropdown.component';
|
||||||
|
|
||||||
|
describe('FilterableDropodownComponent', () => {
|
||||||
|
let component: FilterableDropodownComponent;
|
||||||
|
let fixture: ComponentFixture<FilterableDropodownComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ FilterableDropodownComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(FilterableDropodownComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,267 @@
|
|||||||
|
import { Component, EventEmitter, Input, Output, ElementRef, ViewChild } from '@angular/core';
|
||||||
|
import { FilterPipe } from 'src/app/pipes/filter.pipe';
|
||||||
|
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { ToggleableItemState } from './toggleable-dropdown-button/toggleable-dropdown-button.component';
|
||||||
|
import { MatchingModel } from 'src/app/data/matching-model';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
|
export interface ChangedItems {
|
||||||
|
itemsToAdd: MatchingModel[],
|
||||||
|
itemsToRemove: MatchingModel[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FilterableDropdownSelectionModel {
|
||||||
|
|
||||||
|
changed = new Subject<FilterableDropdownSelectionModel>()
|
||||||
|
|
||||||
|
multiple = false
|
||||||
|
|
||||||
|
items: MatchingModel[] = []
|
||||||
|
|
||||||
|
get itemsSorted(): MatchingModel[] {
|
||||||
|
return this.items.sort((a,b) => {
|
||||||
|
if (this.getNonTemporary(a.id) == ToggleableItemState.NotSelected && this.getNonTemporary(b.id) != ToggleableItemState.NotSelected) {
|
||||||
|
return 1
|
||||||
|
} else if (this.getNonTemporary(a.id) != ToggleableItemState.NotSelected && this.getNonTemporary(b.id) == ToggleableItemState.NotSelected) {
|
||||||
|
return -1
|
||||||
|
} else {
|
||||||
|
return a.name.localeCompare(b.name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private selectionStates = new Map<number, ToggleableItemState>()
|
||||||
|
|
||||||
|
private temporarySelectionStates = new Map<number, ToggleableItemState>()
|
||||||
|
|
||||||
|
getSelectedItems() {
|
||||||
|
return this.items.filter(i => this.temporarySelectionStates.get(i.id) == ToggleableItemState.Selected)
|
||||||
|
}
|
||||||
|
|
||||||
|
set(id: number, state: ToggleableItemState, fireEvent = true) {
|
||||||
|
if (state == ToggleableItemState.NotSelected) {
|
||||||
|
this.temporarySelectionStates.delete(id)
|
||||||
|
} else {
|
||||||
|
this.temporarySelectionStates.set(id, state)
|
||||||
|
}
|
||||||
|
if (fireEvent) {
|
||||||
|
this.changed.next(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle(id: number, fireEvent = true) {
|
||||||
|
let state = this.temporarySelectionStates.get(id)
|
||||||
|
if (state == null || state != ToggleableItemState.Selected) {
|
||||||
|
this.temporarySelectionStates.set(id, ToggleableItemState.Selected)
|
||||||
|
} else if (state == ToggleableItemState.Selected) {
|
||||||
|
this.temporarySelectionStates.delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.multiple) {
|
||||||
|
for (let key of this.temporarySelectionStates.keys()) {
|
||||||
|
if (key != id) {
|
||||||
|
this.temporarySelectionStates.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
for (let key of this.temporarySelectionStates.keys()) {
|
||||||
|
if (key) {
|
||||||
|
this.temporarySelectionStates.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.temporarySelectionStates.delete(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fireEvent) {
|
||||||
|
this.changed.next(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private getNonTemporary(id: number) {
|
||||||
|
return this.selectionStates.get(id) || ToggleableItemState.NotSelected
|
||||||
|
}
|
||||||
|
|
||||||
|
get(id: number) {
|
||||||
|
return this.temporarySelectionStates.get(id) || ToggleableItemState.NotSelected
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionSize() {
|
||||||
|
return this.getSelectedItems().length
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(fireEvent = true) {
|
||||||
|
this.temporarySelectionStates.clear()
|
||||||
|
if (fireEvent) {
|
||||||
|
this.changed.next(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isDirty() {
|
||||||
|
if (!Array.from(this.temporarySelectionStates.keys()).every(id => this.temporarySelectionStates.get(id) == this.selectionStates.get(id))) {
|
||||||
|
return true
|
||||||
|
} else if (!Array.from(this.selectionStates.keys()).every(id => this.selectionStates.get(id) == this.temporarySelectionStates.get(id))) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isNoneSelected() {
|
||||||
|
return this.selectionSize() == 1 && this.get(null) == ToggleableItemState.Selected
|
||||||
|
}
|
||||||
|
|
||||||
|
init(map) {
|
||||||
|
this.temporarySelectionStates = map
|
||||||
|
this.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
apply() {
|
||||||
|
this.selectionStates.clear()
|
||||||
|
this.temporarySelectionStates.forEach((value, key) => {
|
||||||
|
this.selectionStates.set(key, value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.temporarySelectionStates.clear()
|
||||||
|
this.selectionStates.forEach((value, key) => {
|
||||||
|
this.temporarySelectionStates.set(key, value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
diff(): ChangedItems {
|
||||||
|
return {
|
||||||
|
itemsToAdd: this.items.filter(item => this.temporarySelectionStates.get(item.id) == ToggleableItemState.Selected && this.selectionStates.get(item.id) != ToggleableItemState.Selected),
|
||||||
|
itemsToRemove: this.items.filter(item => !this.temporarySelectionStates.has(item.id) && this.selectionStates.has(item.id)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-filterable-dropdown',
|
||||||
|
templateUrl: './filterable-dropdown.component.html',
|
||||||
|
styleUrls: ['./filterable-dropdown.component.scss']
|
||||||
|
})
|
||||||
|
export class FilterableDropdownComponent {
|
||||||
|
|
||||||
|
@ViewChild('listFilterTextInput') listFilterTextInput: ElementRef
|
||||||
|
@ViewChild('dropdown') dropdown: NgbDropdown
|
||||||
|
|
||||||
|
filterText: string
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set items(items: MatchingModel[]) {
|
||||||
|
if (items) {
|
||||||
|
this._selectionModel.items = Array.from(items)
|
||||||
|
this._selectionModel.items.unshift({
|
||||||
|
name: $localize`:Filter drop down element to filter for documents with no correspondent/type/tag assigned:Not assigned`,
|
||||||
|
id: null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get items(): MatchingModel[] {
|
||||||
|
return this._selectionModel.items
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectionModel = new FilterableDropdownSelectionModel()
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set selectionModel(model: FilterableDropdownSelectionModel) {
|
||||||
|
if (this.selectionModel) {
|
||||||
|
this.selectionModel.changed.complete()
|
||||||
|
model.items = this.selectionModel.items
|
||||||
|
model.multiple = this.selectionModel.multiple
|
||||||
|
}
|
||||||
|
model.changed.subscribe(updatedModel => {
|
||||||
|
this.selectionModelChange.next(updatedModel)
|
||||||
|
})
|
||||||
|
this._selectionModel = model
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectionModel(): FilterableDropdownSelectionModel {
|
||||||
|
return this._selectionModel
|
||||||
|
}
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
selectionModelChange = new EventEmitter<FilterableDropdownSelectionModel>()
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set multiple(value: boolean) {
|
||||||
|
this.selectionModel.multiple = value
|
||||||
|
}
|
||||||
|
|
||||||
|
get multiple() {
|
||||||
|
return this.selectionModel.multiple
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
title: string
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
filterPlaceholder: string = ""
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
icon: string
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
allowSelectNone: boolean = false
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
editing = false
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
applyOnClose = false
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
apply = new EventEmitter<ChangedItems>()
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
open = new EventEmitter()
|
||||||
|
|
||||||
|
constructor(private filterPipe: FilterPipe) {
|
||||||
|
this.selectionModel = new FilterableDropdownSelectionModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
applyClicked() {
|
||||||
|
if (this.selectionModel.isDirty()) {
|
||||||
|
this.dropdown.close()
|
||||||
|
if (!this.applyOnClose) {
|
||||||
|
this.apply.emit(this.selectionModel.diff())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dropdownOpenChange(open: boolean): void {
|
||||||
|
if (open) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.listFilterTextInput.nativeElement.focus();
|
||||||
|
}, 0)
|
||||||
|
if (this.editing) {
|
||||||
|
this.selectionModel.reset()
|
||||||
|
}
|
||||||
|
this.open.next()
|
||||||
|
} else {
|
||||||
|
this.filterText = ''
|
||||||
|
if (this.applyOnClose && this.selectionModel.isDirty()) {
|
||||||
|
this.apply.emit(this.selectionModel.diff())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listFilterEnter(): void {
|
||||||
|
let filtered = this.filterPipe.transform(this.items, this.filterText)
|
||||||
|
if (filtered.length == 1) {
|
||||||
|
this.selectionModel.toggle(filtered[0].id)
|
||||||
|
if (this.editing) {
|
||||||
|
this.applyClicked()
|
||||||
|
} else {
|
||||||
|
this.dropdown.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-left-0 border-right-0 border-bottom" role="menuitem" (click)="toggleItem()">
|
||||||
|
<div class="selected-icon mr-1">
|
||||||
|
<ng-container *ngIf="isChecked()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16">
|
||||||
|
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
|
||||||
|
</svg>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="isPartiallyChecked()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-dash" viewBox="0 0 16 16">
|
||||||
|
<path d="M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8z"/>
|
||||||
|
</svg>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="mr-1">
|
||||||
|
<app-tag *ngIf="isTag; else displayName" [tag]="item" [clickable]="true" linkTitle="Filter by tag"></app-tag>
|
||||||
|
<ng-template #displayName><small>{{item.name}}</small></ng-template>
|
||||||
|
</div>
|
||||||
|
<div class="badge badge-light rounded-pill ml-auto mr-1">{{item.document_count}}</div>
|
||||||
|
</button>
|
@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ToggleableDropdownButtonComponent } from './toggleable-dropdown-button.component';
|
||||||
|
|
||||||
|
describe('ToggleableDropdownButtonComponent', () => {
|
||||||
|
let component: ToggleableDropdownButtonComponent;
|
||||||
|
let fixture: ComponentFixture<ToggleableDropdownButtonComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ ToggleableDropdownButtonComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ToggleableDropdownButtonComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,51 @@
|
|||||||
|
import { Component, EventEmitter, Input, Output, OnInit } from '@angular/core';
|
||||||
|
import { MatchingModel } from 'src/app/data/matching-model';
|
||||||
|
|
||||||
|
export interface ToggleableItem {
|
||||||
|
item: MatchingModel,
|
||||||
|
state: ToggleableItemState,
|
||||||
|
count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ToggleableItemState {
|
||||||
|
NotSelected = 0,
|
||||||
|
Selected = 1,
|
||||||
|
PartiallySelected = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-toggleable-dropdown-button',
|
||||||
|
templateUrl: './toggleable-dropdown-button.component.html',
|
||||||
|
styleUrls: ['./toggleable-dropdown-button.component.scss']
|
||||||
|
})
|
||||||
|
export class ToggleableDropdownButtonComponent {
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
item: MatchingModel
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
state: ToggleableItemState
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
count: number
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
toggle = new EventEmitter()
|
||||||
|
|
||||||
|
get isTag(): boolean {
|
||||||
|
return 'is_inbox_tag' in this.item
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleItem(): void {
|
||||||
|
this.toggle.emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
isChecked() {
|
||||||
|
return this.state == ToggleableItemState.Selected
|
||||||
|
}
|
||||||
|
|
||||||
|
isPartiallyChecked() {
|
||||||
|
return this.state == ToggleableItemState.PartiallySelected
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -5,7 +5,9 @@
|
|||||||
<ng-select name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="displayValue"
|
<ng-select name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="displayValue"
|
||||||
[multiple]="true"
|
[multiple]="true"
|
||||||
[closeOnSelect]="false"
|
[closeOnSelect]="false"
|
||||||
|
[clearSearchOnAdd]="true"
|
||||||
[disabled]="disabled"
|
[disabled]="disabled"
|
||||||
|
[hideSelected]="true"
|
||||||
(change)="ngSelectChange()">
|
(change)="ngSelectChange()">
|
||||||
|
|
||||||
<ng-template ng-label-tmp let-item="item">
|
<ng-template ng-label-tmp let-item="item">
|
||||||
|
@ -10,6 +10,6 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-dark" (click)="cancelClicked()">Cancel</button>
|
<button type="button" class="btn btn-outline-dark" (click)="cancelClicked()" i18n>Cancel</button>
|
||||||
<button type="button" class="btn btn-primary" (click)="selectClicked.emit(selected)">Select</button>
|
<button type="button" class="btn btn-primary" (click)="selectClicked.emit(selected)" i18n>Select</button>
|
||||||
</div>
|
</div>
|
@ -15,10 +15,10 @@ export class SelectDialogComponent implements OnInit {
|
|||||||
public selectClicked = new EventEmitter()
|
public selectClicked = new EventEmitter()
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
title = "Select"
|
title = $localize`Select`
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
message = "Please select an object"
|
message = $localize`Please select an object`
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
objects: ObjectWithId[] = []
|
objects: ObjectWithId[] = []
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<app-page-header title="Dashboard" [subTitle]="subtitle">
|
<app-page-header title="Dashboard" [subTitle]="subtitle" i18n-title>
|
||||||
<img src="assets/logo.svg" height="80" class="m-2 d-none d-md-block">
|
<img src="assets/logo.svg" height="80" class="m-2 d-none d-md-block">
|
||||||
</app-page-header>
|
</app-page-header>
|
||||||
|
|
||||||
|
@ -30,9 +30,9 @@ export class DashboardComponent implements OnInit {
|
|||||||
|
|
||||||
get subtitle() {
|
get subtitle() {
|
||||||
if (this.displayName) {
|
if (this.displayName) {
|
||||||
return `Hello ${this.displayName}, welcome to Paperless-ng!`
|
return $localize`Hello ${this.displayName}, welcome to Paperless-ng!`
|
||||||
} else {
|
} else {
|
||||||
return `Welcome to Paperless-ng!`
|
return $localize`Welcome to Paperless-ng!`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
<app-widget-frame [title]="savedView.name">
|
<app-widget-frame [title]="savedView.name">
|
||||||
|
|
||||||
<a header-buttons [routerLink]="" (click)="showAll()">Show all</a>
|
<a header-buttons [routerLink]="" (click)="showAll()" i18n>Show all</a>
|
||||||
|
|
||||||
|
|
||||||
<table content class="table table-sm table-hover table-borderless">
|
<table content class="table table-sm table-hover table-borderless">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Created</th>
|
<th i18n>Created</th>
|
||||||
<th scope="col">Title</th>
|
<th scope="col" i18n>Title</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<app-widget-frame title="Statistics">
|
<app-widget-frame title="Statistics" i18n-title>
|
||||||
<ng-container content>
|
<ng-container content>
|
||||||
<p class="card-text">Documents in inbox: {{statistics.documents_inbox}}</p>
|
<p class="card-text" i18n>Documents in inbox: {{statistics.documents_inbox}}</p>
|
||||||
<p class="card-text">Total documents: {{statistics.documents_total}}</p>
|
<p class="card-text" i18n>Total documents: {{statistics.documents_total}}</p>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</app-widget-frame>
|
</app-widget-frame>
|
@ -1,16 +1,16 @@
|
|||||||
<app-widget-frame title="Upload new documents">
|
<app-widget-frame title="Upload new documents" i18n-title>
|
||||||
|
|
||||||
<div content>
|
<div content>
|
||||||
<form>
|
<form>
|
||||||
<ngx-file-drop dropZoneLabel="Drop documents here or" (onFileDrop)="dropped($event)"
|
<ngx-file-drop dropZoneLabel="Drop documents here or" browseBtnLabel="Browse files" (onFileDrop)="dropped($event)"
|
||||||
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" dropZoneClassName="bg-light card"
|
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" dropZoneClassName="bg-light card"
|
||||||
multiple="true" contentClassName="justify-content-center d-flex align-items-center p-5" [showBrowseBtn]=true
|
multiple="true" contentClassName="justify-content-center d-flex align-items-center p-5" [showBrowseBtn]=true
|
||||||
browseBtnClassName="btn btn-sm btn-outline-primary ml-2">
|
browseBtnClassName="btn btn-sm btn-outline-primary ml-2" i18n-dropZoneLabel i18n-browseBtnLabel>
|
||||||
|
|
||||||
</ngx-file-drop>
|
</ngx-file-drop>
|
||||||
</form>
|
</form>
|
||||||
<div *ngIf="uploadVisible" class="mt-3">
|
<div *ngIf="uploadVisible" class="mt-3">
|
||||||
<p>Uploading {{uploadStatus.length}} file(s)</p>
|
<p i18n>Uploading {uploadStatus.length, plural, =1 {file} =other {{{uploadStatus.length}} files}}...</p>
|
||||||
<ngb-progressbar [value]="loadedSum" [max]="totalSum" [striped]="true" [animated]="uploadStatus.length > 0">
|
<ngb-progressbar [value]="loadedSum" [max]="totalSum" [striped]="true" [animated]="uploadStatus.length > 0">
|
||||||
</ngb-progressbar>
|
</ngb-progressbar>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,7 +2,7 @@ import { HttpEventType } from '@angular/common/http';
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop';
|
import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop';
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service';
|
import { DocumentService } from 'src/app/services/rest/document.service';
|
||||||
import { Toast, ToastService } from 'src/app/services/toast.service';
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
|
|
||||||
|
|
||||||
interface UploadStatus {
|
interface UploadStatus {
|
||||||
@ -60,7 +60,7 @@ export class UploadFileWidgetComponent implements OnInit {
|
|||||||
} else if (event.type == HttpEventType.Response) {
|
} else if (event.type == HttpEventType.Response) {
|
||||||
this.uploadStatus.splice(this.uploadStatus.indexOf(uploadStatusObject), 1)
|
this.uploadStatus.splice(this.uploadStatus.indexOf(uploadStatusObject), 1)
|
||||||
this.completedFiles += 1
|
this.completedFiles += 1
|
||||||
this.toastService.showToast(Toast.make("Information", "The document has been uploaded and will be processed by the consumer shortly."))
|
this.toastService.showInfo($localize`The document has been uploaded and will be processed by the consumer shortly.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
}, error => {
|
}, error => {
|
||||||
@ -68,11 +68,11 @@ export class UploadFileWidgetComponent implements OnInit {
|
|||||||
this.completedFiles += 1
|
this.completedFiles += 1
|
||||||
switch (error.status) {
|
switch (error.status) {
|
||||||
case 400: {
|
case 400: {
|
||||||
this.toastService.showToast(Toast.makeError(`There was an error while uploading the document: ${error.error.document}`))
|
this.toastService.showInfo($localize`There was an error while uploading the document: ${error.error.document}`)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
this.toastService.showToast(Toast.makeError("An error has occurred while uploading the document. Sorry!"))
|
this.toastService.showInfo($localize`An error has occurred while uploading the document. Sorry!`)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
<app-widget-frame title="First steps">
|
<app-widget-frame title="First steps" i18n-title>
|
||||||
|
|
||||||
<ng-container content>
|
<ng-container content>
|
||||||
<img src="assets/save-filter.png" class="float-right">
|
<img src="assets/save-filter.png" class="float-right">
|
||||||
<p>Paperless is running! :)</p>
|
<p i18n>Paperless is running! :)</p>
|
||||||
<p>You can start uploading documents by dropping them in the file upload box to the right or by dropping them in the configured consumption folder and they'll start showing up in the documents list.
|
<p i18n>You can start uploading documents by dropping them in the file upload box to the right or by dropping them in the configured consumption folder and they'll start showing up in the documents list.
|
||||||
After you've added some metadata to your documents, use the filtering mechanisms of paperless to create custom views (such as 'Recently added', 'Tagged TODO') and have them displayed on the dashboard instead of this message.</p>
|
After you've added some metadata to your documents, use the filtering mechanisms of paperless to create custom views (such as 'Recently added', 'Tagged TODO') and they will appear on the dashboard instead of this message.</p>
|
||||||
<p>Paperless offers some more features that try to make your life easier, such as:</p>
|
<p i18n>Paperless offers some more features that try to make your life easier:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Once you've got a couple documents in paperless and added metadata to them, paperless can assign that metadata to new documents automatically.</li>
|
<li i18n>Once you've got a couple documents in paperless and added metadata to them, paperless can assign that metadata to new documents automatically.</li>
|
||||||
<li>You can configure paperless to read your mails and add documents from attached files.</li>
|
<li i18n>You can configure paperless to read your mails and add documents from attached files.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>Consult the documentation on how to use these features. The section on basic usage also has some information on how to use paperless in general.</p>
|
<p i18n>Consult the documentation on how to use these features. The section on basic usage also has some information on how to use paperless in general.</p>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
</app-widget-frame>
|
</app-widget-frame>
|
@ -1,19 +1,18 @@
|
|||||||
<app-page-header [(title)]="title">
|
<app-page-header [(title)]="title">
|
||||||
<div class="input-group input-group-sm mr-5" *ngIf="getContentType() == 'application/pdf'">
|
<div class="input-group input-group-sm mr-5" *ngIf="getContentType() == 'application/pdf'">
|
||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
<div class="input-group-text">Page </div>
|
<div class="input-group-text" i18n>Page</div>
|
||||||
</div>
|
</div>
|
||||||
<input class="form-control flex-grow-0 w-auto" type="number" min="1" [max]="previewNumPages" [(ngModel)]="previewCurrentPage" />
|
<input class="form-control flex-grow-0 w-auto" type="number" min="1" [max]="previewNumPages" [(ngModel)]="previewCurrentPage" />
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<div class="input-group-text">of {{previewNumPages}}</div>
|
<div class="input-group-text" i18n>of {{previewNumPages}}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger mr-2" (click)="delete()">
|
<button type="button" class="btn btn-sm btn-outline-danger mr-2" (click)="delete()">
|
||||||
<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" />
|
||||||
</svg>
|
</svg> <span class="d-none d-lg-inline" i18n>Delete</span>
|
||||||
<span class="d-none d-lg-inline"> Delete</span>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="btn-group mr-2">
|
<div class="btn-group mr-2">
|
||||||
@ -21,14 +20,13 @@
|
|||||||
<a [href]="downloadUrl" class="btn btn-sm btn-outline-primary">
|
<a [href]="downloadUrl" class="btn btn-sm btn-outline-primary">
|
||||||
<svg class="buttonicon" fill="currentColor">
|
<svg class="buttonicon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#download" />
|
<use xlink:href="assets/bootstrap-icons.svg#download" />
|
||||||
</svg>
|
</svg> <span class="d-none d-lg-inline" i18n>Download</span>
|
||||||
<span class="d-none d-lg-inline"> Download</span>
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="btn-group" ngbDropdown role="group" *ngIf="metadata?.has_archive_version">
|
<div class="btn-group" ngbDropdown role="group" *ngIf="metadata?.has_archive_version">
|
||||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle-split" ngbDropdownToggle></button>
|
<button class="btn btn-sm btn-outline-primary dropdown-toggle-split" ngbDropdownToggle></button>
|
||||||
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
||||||
<a ngbDropdownItem [href]="downloadOriginalUrl">Download original</a>
|
<a ngbDropdownItem [href]="downloadOriginalUrl" i18n>Download original</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -37,15 +35,13 @@
|
|||||||
<button type="button" class="btn btn-sm btn-outline-primary mr-2" (click)="moreLike()">
|
<button type="button" class="btn btn-sm btn-outline-primary mr-2" (click)="moreLike()">
|
||||||
<svg class="buttonicon" fill="currentColor">
|
<svg class="buttonicon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#three-dots" />
|
<use xlink:href="assets/bootstrap-icons.svg#three-dots" />
|
||||||
</svg>
|
</svg> <span class="d-none d-lg-inline" i18n>More like this</span>
|
||||||
<span class="d-none d-lg-inline"> More like this</span>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="close()">
|
<button type="button" class="btn btn-sm btn-outline-primary" (click)="close()">
|
||||||
<svg class="buttonicon" fill="currentColor">
|
<svg class="buttonicon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#x" />
|
<use xlink:href="assets/bootstrap-icons.svg#x" />
|
||||||
</svg>
|
</svg> <span class="d-none d-lg-inline" i18n>Close</span>
|
||||||
<span class="d-none d-lg-inline"> Close</span>
|
|
||||||
</button>
|
</button>
|
||||||
</app-page-header>
|
</app-page-header>
|
||||||
|
|
||||||
@ -57,27 +53,27 @@
|
|||||||
|
|
||||||
<ul ngbNav #nav="ngbNav" class="nav-tabs">
|
<ul ngbNav #nav="ngbNav" class="nav-tabs">
|
||||||
<li [ngbNavItem]="1">
|
<li [ngbNavItem]="1">
|
||||||
<a ngbNavLink>Details</a>
|
<a ngbNavLink i18n>Details</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
|
|
||||||
<app-input-text title="Title" formControlName="title"></app-input-text>
|
<app-input-text i18n-title title="Title" formControlName="title"></app-input-text>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="archive_serial_number">Archive Serial Number</label>
|
<label for="archive_serial_number" i18n>Archive serial number</label>
|
||||||
<input type="number" class="form-control" id="archive_serial_number"
|
<input type="number" class="form-control" id="archive_serial_number"
|
||||||
formControlName='archive_serial_number'>
|
formControlName='archive_serial_number'>
|
||||||
</div>
|
</div>
|
||||||
<app-input-date-time titleDate="Date created" formControlName="created"></app-input-date-time>
|
<app-input-date-time i18n-titleDate titleDate="Date created" formControlName="created"></app-input-date-time>
|
||||||
<app-input-select [items]="correspondents" title="Correspondent" formControlName="correspondent" [allowNull]="true"
|
<app-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true"
|
||||||
(createNew)="createCorrespondent()"></app-input-select>
|
(createNew)="createCorrespondent()"></app-input-select>
|
||||||
<app-input-select [items]="documentTypes" title="Document type" formControlName="document_type" [allowNull]="true"
|
<app-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true"
|
||||||
(createNew)="createDocumentType()"></app-input-select>
|
(createNew)="createDocumentType()"></app-input-select>
|
||||||
<app-input-tags formControlName="tags" title="Tags"></app-input-tags>
|
<app-input-tags formControlName="tags" i18n-title title="Tags"></app-input-tags>
|
||||||
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li [ngbNavItem]="2">
|
<li [ngbNavItem]="2">
|
||||||
<a ngbNavLink>Content</a>
|
<a ngbNavLink i18n>Content</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<textarea class="form-control" id="content" rows="20" formControlName='content'></textarea>
|
<textarea class="form-control" id="content" rows="20" formControlName='content'></textarea>
|
||||||
@ -86,48 +82,48 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li [ngbNavItem]="3">
|
<li [ngbNavItem]="3">
|
||||||
<a ngbNavLink>Metadata</a>
|
<a ngbNavLink i18n>Metadata</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
|
|
||||||
<table class="table table-borderless">
|
<table class="table table-borderless">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Date modified</td>
|
<td i18n>Date modified</td>
|
||||||
<td>{{document.modified | date:'medium'}}</td>
|
<td>{{document.modified | date:'medium'}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Date added</td>
|
<td i18n>Date added</td>
|
||||||
<td>{{document.added | date:'medium'}}</td>
|
<td>{{document.added | date:'medium'}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Media filename</td>
|
<td i18n>Media filename</td>
|
||||||
<td>{{metadata?.media_filename}}</td>
|
<td>{{metadata?.media_filename}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Original MD5 Checksum</td>
|
<td i18n>Original MD5 checksum</td>
|
||||||
<td>{{metadata?.original_checksum}}</td>
|
<td>{{metadata?.original_checksum}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Original file size</td>
|
<td i18n>Original file size</td>
|
||||||
<td>{{metadata?.original_size | fileSize}}</td>
|
<td>{{metadata?.original_size | fileSize}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Original mime type</td>
|
<td i18n>Original mime type</td>
|
||||||
<td>{{metadata?.original_mime_type}}</td>
|
<td>{{metadata?.original_mime_type}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="metadata?.has_archive_version">
|
<tr *ngIf="metadata?.has_archive_version">
|
||||||
<td>Archive MD5 Checksum</td>
|
<td i18n>Archive MD5 checksum</td>
|
||||||
<td>{{metadata?.archive_checksum}}</td>
|
<td>{{metadata?.archive_checksum}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="metadata?.has_archive_version">
|
<tr *ngIf="metadata?.has_archive_version">
|
||||||
<td>Archive file size</td>
|
<td i18n>Archive file size</td>
|
||||||
<td>{{metadata?.archive_size | fileSize}}</td>
|
<td>{{metadata?.archive_size | fileSize}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<app-metadata-collapse title="Original document metadata" [metadata]="metadata.original_metadata" *ngIf="metadata?.original_metadata?.length > 0"></app-metadata-collapse>
|
<app-metadata-collapse i18n-title title="Original document metadata" [metadata]="metadata.original_metadata" *ngIf="metadata?.original_metadata?.length > 0"></app-metadata-collapse>
|
||||||
<app-metadata-collapse title="Archived document metadata" [metadata]="metadata.archive_metadata" *ngIf="metadata?.archive_metadata?.length > 0"></app-metadata-collapse>
|
<app-metadata-collapse i18n-title title="Archived document metadata" [metadata]="metadata.archive_metadata" *ngIf="metadata?.archive_metadata?.length > 0"></app-metadata-collapse>
|
||||||
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
@ -135,16 +131,15 @@
|
|||||||
|
|
||||||
<div [ngbNavOutlet]="nav" class="mt-2"></div>
|
<div [ngbNavOutlet]="nav" class="mt-2"></div>
|
||||||
|
|
||||||
<button type="button" class="btn btn-outline-secondary" (click)="discard()">Discard</button>
|
<button type="button" class="btn btn-outline-secondary" (click)="discard()" i18n>Discard</button>
|
||||||
<button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()">Save & edit
|
<button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()" i18n>Save & next</button>
|
||||||
next</button>
|
<button type="submit" class="btn btn-primary" i18n>Save</button>
|
||||||
<button type="submit" class="btn btn-primary">Save</button>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6 col-xl-8 mb-3">
|
<div class="col-md-6 col-xl-8 mb-3">
|
||||||
<div class="pdf-viewer-container" *ngIf="getContentType() == 'application/pdf'">
|
<div class="pdf-viewer-container" *ngIf="getContentType() == 'application/pdf'">
|
||||||
<pdf-viewer [src]="previewUrl" [original-size]="false" [show-borders]="true" [show-all]="true" [(page)]="previewCurrentPage" (after-load-complete)="pdfPreviewLoaded($event)"></pdf-viewer>
|
<pdf-viewer [src]="previewUrl" [original-size]="false" [show-borders]="true" [show-all]="true" [(page)]="previewCurrentPage" [render-text-mode]="2" (after-load-complete)="pdfPreviewLoaded($event)"></pdf-viewer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -158,11 +158,11 @@ export class DocumentDetailComponent implements OnInit {
|
|||||||
|
|
||||||
delete() {
|
delete() {
|
||||||
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
||||||
modal.componentInstance.title = "Confirm delete"
|
modal.componentInstance.title = $localize`Confirm delete`
|
||||||
modal.componentInstance.messageBold = `Do you really want to delete document '${this.document.title}'?`
|
modal.componentInstance.messageBold = $localize`Do you really want to delete document "${this.document.title}"?`
|
||||||
modal.componentInstance.message = `The files for this document will be deleted permanently. This operation cannot be undone.`
|
modal.componentInstance.message = $localize`The files for this document will be deleted permanently. This operation cannot be undone.`
|
||||||
modal.componentInstance.btnClass = "btn-danger"
|
modal.componentInstance.btnClass = "btn-danger"
|
||||||
modal.componentInstance.btnCaption = "Delete document"
|
modal.componentInstance.btnCaption = $localize`Delete document`
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
this.documentsService.delete(this.document).subscribe(() => {
|
this.documentsService.delete(this.document).subscribe(() => {
|
||||||
modal.close()
|
modal.close()
|
||||||
|
@ -15,7 +15,7 @@ export class MetadataCollapseComponent implements OnInit {
|
|||||||
metadata
|
metadata
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
title = "Metadata"
|
title = $localize`Metadata`
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,67 @@
|
|||||||
|
<div class="row">
|
||||||
|
<div class="col-auto mb-2 mb-xl-0" role="group" aria-label="Select">
|
||||||
|
<button class="btn btn-sm btn-outline-danger" (click)="list.selectNone()">
|
||||||
|
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#slash-circle" />
|
||||||
|
</svg> <ng-container i18n>Cancel</ng-container>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="w-100 d-xl-none"></div>
|
||||||
|
<div class="col-auto mb-2 mb-xl-0" role="group" aria-label="Select">
|
||||||
|
<label class="mr-2 mb-0" i18n>Select:</label>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-sm btn-outline-primary" (click)="list.selectPage()">
|
||||||
|
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#file-earmark-check" />
|
||||||
|
</svg> <ng-container i18n>Page</ng-container>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-primary" (click)="list.selectAll()">
|
||||||
|
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#check-all" />
|
||||||
|
</svg> <ng-container i18n>All</ng-container>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-100 d-xl-none"></div>
|
||||||
|
<div class="col-auto mb-2 mb-xl-0">
|
||||||
|
<div class="d-flex">
|
||||||
|
<label class="ml-auto mt-1 mb-0 mr-2" i18n>Edit:</label>
|
||||||
|
<app-filterable-dropdown class="mr-2 mr-md-3" title="Tags" icon="tag-fill" i18n-title
|
||||||
|
filterPlaceholder="Filter tags" i18n-filterPlaceholder
|
||||||
|
[items]="tags"
|
||||||
|
[editing]="true"
|
||||||
|
[multiple]="true"
|
||||||
|
[applyOnClose]="applyOnClose"
|
||||||
|
(open)="openTagsDropdown()"
|
||||||
|
[(selectionModel)]="tagSelectionModel"
|
||||||
|
(apply)="setTags($event)">
|
||||||
|
</app-filterable-dropdown>
|
||||||
|
<app-filterable-dropdown class="mr-2 mr-md-3" title="Correspondent" icon="person-fill" i18n-title
|
||||||
|
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
|
||||||
|
[items]="correspondents"
|
||||||
|
[editing]="true"
|
||||||
|
[applyOnClose]="applyOnClose"
|
||||||
|
(open)="openCorrespondentDropdown()"
|
||||||
|
[(selectionModel)]="correspondentSelectionModel"
|
||||||
|
(apply)="setCorrespondents($event)">
|
||||||
|
</app-filterable-dropdown>
|
||||||
|
<app-filterable-dropdown class="mr-2 mr-md-3" title="Document type" icon="file-earmark-fill" i18n-title
|
||||||
|
filterPlaceholder="Filter document types" i18n-filterPlaceholder
|
||||||
|
[items]="documentTypes"
|
||||||
|
[editing]="true"
|
||||||
|
[applyOnClose]="applyOnClose"
|
||||||
|
(open)="openDocumentTypeDropdown()"
|
||||||
|
[(selectionModel)]="documentTypeSelectionModel"
|
||||||
|
(apply)="setDocumentTypes($event)">
|
||||||
|
</app-filterable-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-100 d-xl-none"></div>
|
||||||
|
<div class="col mb-2 mb-xl-0 d-flex">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger ml-0 ml-lg-auto" (click)="applyDelete()">
|
||||||
|
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||||
|
</svg> <ng-container i18n>Delete</ng-container>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -1,20 +1,20 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { FilterDropdownDateComponent } from './filter-dropdown-date.component';
|
import { BulkEditorComponent } from './bulk-editor.component';
|
||||||
|
|
||||||
describe('FilterDropdownDateComponent', () => {
|
describe('BulkEditorComponent', () => {
|
||||||
let component: FilterDropdownDateComponent;
|
let component: BulkEditorComponent;
|
||||||
let fixture: ComponentFixture<FilterDropdownDateComponent>;
|
let fixture: ComponentFixture<BulkEditorComponent>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [ FilterDropdownDateComponent ]
|
declarations: [ BulkEditorComponent ]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(FilterDropdownDateComponent);
|
fixture = TestBed.createComponent(BulkEditorComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
@ -0,0 +1,232 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { tap } from 'rxjs/operators';
|
||||||
|
import { PaperlessTag } from 'src/app/data/paperless-tag';
|
||||||
|
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
|
||||||
|
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
|
||||||
|
import { TagService } from 'src/app/services/rest/tag.service';
|
||||||
|
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
|
||||||
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service';
|
||||||
|
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { DocumentService, SelectionDataItem } from 'src/app/services/rest/document.service';
|
||||||
|
import { OpenDocumentsService } from 'src/app/services/open-documents.service';
|
||||||
|
import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component';
|
||||||
|
import { ChangedItems, FilterableDropdownSelectionModel } from '../../common/filterable-dropdown/filterable-dropdown.component';
|
||||||
|
import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component';
|
||||||
|
import { MatchingModel } from 'src/app/data/matching-model';
|
||||||
|
import { SettingsService, SETTINGS_KEYS } from 'src/app/services/settings.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-bulk-editor',
|
||||||
|
templateUrl: './bulk-editor.component.html',
|
||||||
|
styleUrls: ['./bulk-editor.component.scss']
|
||||||
|
})
|
||||||
|
export class BulkEditorComponent {
|
||||||
|
|
||||||
|
tags: PaperlessTag[]
|
||||||
|
correspondents: PaperlessCorrespondent[]
|
||||||
|
documentTypes: PaperlessDocumentType[]
|
||||||
|
|
||||||
|
tagSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
|
correspondentSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
|
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private documentTypeService: DocumentTypeService,
|
||||||
|
private tagService: TagService,
|
||||||
|
private correspondentService: CorrespondentService,
|
||||||
|
public list: DocumentListViewService,
|
||||||
|
private documentService: DocumentService,
|
||||||
|
private modalService: NgbModal,
|
||||||
|
private openDocumentService: OpenDocumentsService,
|
||||||
|
private settings: SettingsService
|
||||||
|
) { }
|
||||||
|
|
||||||
|
applyOnClose: boolean = this.settings.get(SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE)
|
||||||
|
showConfirmationDialogs: boolean = this.settings.get(SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS)
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.tagService.listAll().subscribe(result => this.tags = result.results)
|
||||||
|
this.correspondentService.listAll().subscribe(result => this.correspondents = result.results)
|
||||||
|
this.documentTypeService.listAll().subscribe(result => this.documentTypes = result.results)
|
||||||
|
}
|
||||||
|
|
||||||
|
private executeBulkOperation(method: string, args): Observable<any> {
|
||||||
|
return this.documentService.bulkEdit(Array.from(this.list.selected), method, args).pipe(
|
||||||
|
tap(() => {
|
||||||
|
this.list.reload()
|
||||||
|
this.list.reduceSelectionToFilter()
|
||||||
|
this.list.selected.forEach(id => {
|
||||||
|
this.openDocumentService.refreshDocument(id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private applySelectionData(items: SelectionDataItem[], selectionModel: FilterableDropdownSelectionModel) {
|
||||||
|
let selectionData = new Map<number, ToggleableItemState>()
|
||||||
|
items.forEach(i => {
|
||||||
|
if (i.document_count == this.list.selected.size) {
|
||||||
|
selectionData.set(i.id, ToggleableItemState.Selected)
|
||||||
|
} else if (i.document_count > 0) {
|
||||||
|
selectionData.set(i.id, ToggleableItemState.PartiallySelected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
selectionModel.init(selectionData)
|
||||||
|
}
|
||||||
|
|
||||||
|
openTagsDropdown() {
|
||||||
|
this.documentService.getSelectionData(Array.from(this.list.selected)).subscribe(s => {
|
||||||
|
this.applySelectionData(s.selected_tags, this.tagSelectionModel)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
openDocumentTypeDropdown() {
|
||||||
|
this.documentService.getSelectionData(Array.from(this.list.selected)).subscribe(s => {
|
||||||
|
this.applySelectionData(s.selected_document_types, this.documentTypeSelectionModel)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
openCorrespondentDropdown() {
|
||||||
|
this.documentService.getSelectionData(Array.from(this.list.selected)).subscribe(s => {
|
||||||
|
this.applySelectionData(s.selected_correspondents, this.correspondentSelectionModel)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private _localizeList(items: MatchingModel[]) {
|
||||||
|
if (items.length == 0) {
|
||||||
|
return ""
|
||||||
|
} else if (items.length == 1) {
|
||||||
|
return items[0].name
|
||||||
|
} else if (items.length == 2) {
|
||||||
|
return $localize`:This is for messages like 'modify "tag1" and "tag2"':"${items[0].name}" and "${items[1].name}"`
|
||||||
|
} else {
|
||||||
|
let list = items.slice(0, items.length - 1).map(i => $localize`"${i.name}"`).join($localize`:this is used to separate enumerations and should probably be a comma and a whitespace in most languages:, `)
|
||||||
|
return $localize`:this is for messages like 'modify "tag1", "tag2" and "tag3"':${list} and "${items[items.length - 1].name}"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTags(changedTags: ChangedItems) {
|
||||||
|
if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length == 0) return
|
||||||
|
|
||||||
|
if (this.showConfirmationDialogs) {
|
||||||
|
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
||||||
|
modal.componentInstance.title = $localize`Confirm tags assignment`
|
||||||
|
if (changedTags.itemsToAdd.length == 1 && changedTags.itemsToRemove.length == 0) {
|
||||||
|
let tag = changedTags.itemsToAdd[0]
|
||||||
|
modal.componentInstance.message = $localize`This operation will add the tag "${tag.name}" to ${this.list.selected.size} selected document(s).`
|
||||||
|
} else if (changedTags.itemsToAdd.length > 1 && changedTags.itemsToRemove.length == 0) {
|
||||||
|
modal.componentInstance.message = $localize`This operation will add the tags ${this._localizeList(changedTags.itemsToAdd)} to ${this.list.selected.size} selected document(s).`
|
||||||
|
} else if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length == 1) {
|
||||||
|
let tag = changedTags.itemsToRemove[0]
|
||||||
|
modal.componentInstance.message = $localize`This operation will remove the tag "${tag.name}" from ${this.list.selected.size} selected document(s).`
|
||||||
|
} else if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length > 1) {
|
||||||
|
modal.componentInstance.message = $localize`This operation will remove the tags ${this._localizeList(changedTags.itemsToRemove)} from ${this.list.selected.size} selected document(s).`
|
||||||
|
} else {
|
||||||
|
modal.componentInstance.message = $localize`This operation will add the tags ${this._localizeList(changedTags.itemsToAdd)} and remove the tags ${this._localizeList(changedTags.itemsToRemove)} on ${this.list.selected.size} selected document(s).`
|
||||||
|
}
|
||||||
|
|
||||||
|
modal.componentInstance.btnClass = "btn-warning"
|
||||||
|
modal.componentInstance.btnCaption = $localize`Confirm`
|
||||||
|
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
|
this.performSetTags(modal, changedTags)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.performSetTags(null, changedTags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private performSetTags(modal, changedTags: ChangedItems) {
|
||||||
|
this.executeBulkOperation('modify_tags', {"add_tags": changedTags.itemsToAdd.map(t => t.id), "remove_tags": changedTags.itemsToRemove.map(t => t.id)}).subscribe(
|
||||||
|
response => {
|
||||||
|
if (modal) {
|
||||||
|
modal.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setCorrespondents(changedCorrespondents: ChangedItems) {
|
||||||
|
if (changedCorrespondents.itemsToAdd.length == 0 && changedCorrespondents.itemsToRemove.length == 0) return
|
||||||
|
|
||||||
|
let correspondent = changedCorrespondents.itemsToAdd.length > 0 ? changedCorrespondents.itemsToAdd[0] : null
|
||||||
|
|
||||||
|
if (this.showConfirmationDialogs) {
|
||||||
|
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
||||||
|
modal.componentInstance.title = $localize`Confirm correspondent assignment`
|
||||||
|
if (correspondent) {
|
||||||
|
modal.componentInstance.message = $localize`This operation will assign the correspondent "${correspondent.name}" to ${this.list.selected.size} selected document(s).`
|
||||||
|
} else {
|
||||||
|
modal.componentInstance.message = $localize`This operation will remove the correspondent from ${this.list.selected.size} selected document(s).`
|
||||||
|
}
|
||||||
|
modal.componentInstance.btnClass = "btn-warning"
|
||||||
|
modal.componentInstance.btnCaption = $localize`Confirm`
|
||||||
|
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
|
this.performSetCorrespondents(modal, correspondent)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.performSetCorrespondents(null, correspondent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private performSetCorrespondents(modal, correspondent: MatchingModel) {
|
||||||
|
this.executeBulkOperation('set_correspondent', {"correspondent": correspondent ? correspondent.id : null}).subscribe(
|
||||||
|
response => {
|
||||||
|
if (modal) {
|
||||||
|
modal.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setDocumentTypes(changedDocumentTypes: ChangedItems) {
|
||||||
|
if (changedDocumentTypes.itemsToAdd.length == 0 && changedDocumentTypes.itemsToRemove.length == 0) return
|
||||||
|
|
||||||
|
let documentType = changedDocumentTypes.itemsToAdd.length > 0 ? changedDocumentTypes.itemsToAdd[0] : null
|
||||||
|
|
||||||
|
if (this.showConfirmationDialogs) {
|
||||||
|
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
||||||
|
modal.componentInstance.title = $localize`Confirm document type assignment`
|
||||||
|
if (documentType) {
|
||||||
|
modal.componentInstance.message = $localize`This operation will assign the document type "${documentType.name}" to ${this.list.selected.size} selected document(s).`
|
||||||
|
} else {
|
||||||
|
modal.componentInstance.message = $localize`This operation will remove the document type from ${this.list.selected.size} selected document(s).`
|
||||||
|
}
|
||||||
|
modal.componentInstance.btnClass = "btn-warning"
|
||||||
|
modal.componentInstance.btnCaption = $localize`Confirm`
|
||||||
|
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
|
this.performSetDocumentTypes(modal, documentType)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.performSetDocumentTypes(null, documentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private performSetDocumentTypes(modal, documentType) {
|
||||||
|
this.executeBulkOperation('set_document_type', {"document_type": documentType ? documentType.id : null}).subscribe(
|
||||||
|
response => {
|
||||||
|
if (modal) {
|
||||||
|
modal.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
applyDelete() {
|
||||||
|
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
||||||
|
modal.componentInstance.delayConfirm(5)
|
||||||
|
modal.componentInstance.title = $localize`Delete confirm`
|
||||||
|
modal.componentInstance.messageBold = $localize`This operation will permanently delete ${this.list.selected.size} selected document(s).`
|
||||||
|
modal.componentInstance.message = $localize`This operation cannot be undone.`
|
||||||
|
modal.componentInstance.btnClass = "btn-danger"
|
||||||
|
modal.componentInstance.btnCaption = $localize`Delete document(s)`
|
||||||
|
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
|
this.executeBulkOperation("delete", {}).subscribe(
|
||||||
|
response => {
|
||||||
|
modal.close()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,11 @@
|
|||||||
<div class="card mb-3 bg-light shadow-sm" [class.card-selected]="selected" [class.document-card]="selectable">
|
<div class="card mb-3 bg-light shadow-sm" [class.card-selected]="selected" [class.document-card]="selectable">
|
||||||
<div class="row no-gutters">
|
<div class="row no-gutters">
|
||||||
<div class="col-md-2 d-none d-lg-block doc-img-background" [class.doc-img-background-selected]="selected">
|
<div class="col-md-2 d-none d-lg-block doc-img-background" [class.doc-img-background-selected]="selected">
|
||||||
<img [src]="getThumbUrl()" class="card-img doc-img border-right" (click)="selected = selectable ? !selected : false">
|
<img [src]="getThumbUrl()" class="card-img doc-img border-right" (click)="setSelected(selectable ? !selected : false)">
|
||||||
|
|
||||||
<div style="top: 0; left: 0" class="position-absolute border-right border-bottom bg-light p-1" [class.document-card-check]="!selected">
|
<div style="top: 0; left: 0" class="position-absolute border-right border-bottom bg-light p-1" [class.document-card-check]="!selected">
|
||||||
<div class="custom-control custom-checkbox">
|
<div class="custom-control custom-checkbox">
|
||||||
<input type="checkbox" class="custom-control-input" id="smallCardCheck{{document.id}}" [checked]="selected" (change)="selected = $event.target.checked">
|
<input type="checkbox" class="custom-control-input" id="smallCardCheck{{document.id}}" [checked]="selected" (change)="setSelected($event.target.checked)">
|
||||||
<label class="custom-control-label" for="smallCardCheck{{document.id}}"></label>
|
<label class="custom-control-label" for="smallCardCheck{{document.id}}"></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -17,11 +17,11 @@
|
|||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<h5 class="card-title">
|
<h5 class="card-title">
|
||||||
<ng-container *ngIf="document.correspondent">
|
<ng-container *ngIf="document.correspondent">
|
||||||
<a *ngIf="clickCorrespondent.observers.length ; else nolink" [routerLink]="" title="Filter by correspondent" (click)="clickCorrespondent.emit(document.correspondent)" class="font-weight-bold">{{(document.correspondent$ | async)?.name}}</a>
|
<a *ngIf="clickCorrespondent.observers.length ; else nolink" [routerLink]="" title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent)" class="font-weight-bold">{{(document.correspondent$ | async)?.name}}</a>
|
||||||
<ng-template #nolink>{{(document.correspondent$ | async)?.name}}</ng-template>:
|
<ng-template #nolink>{{(document.correspondent$ | async)?.name}}</ng-template>:
|
||||||
</ng-container>
|
</ng-container>
|
||||||
{{document.title | documentTitle}}
|
{{document.title | documentTitle}}
|
||||||
<app-tag [tag]="t" linkTitle="Filter by tag" *ngFor="let t of document.tags$ | async" class="ml-1" (click)="clickTag.emit(t.id)" [clickable]="clickTag.observers.length"></app-tag>
|
<app-tag [tag]="t" linkTitle="Filter by tag" i18n-linkTitle *ngFor="let t of document.tags$ | async" class="ml-1" (click)="clickTag.emit(t.id)" [clickable]="clickTag.observers.length"></app-tag>
|
||||||
</h5>
|
</h5>
|
||||||
<h5 class="card-title" *ngIf="document.archive_serial_number">#{{document.archive_serial_number}}</h5>
|
<h5 class="card-title" *ngIf="document.archive_serial_number">#{{document.archive_serial_number}}</h5>
|
||||||
</div>
|
</div>
|
||||||
@ -36,37 +36,33 @@
|
|||||||
<a routerLink="/search" [queryParams]="{'more_like': document.id}" class="btn btn-sm btn-outline-secondary" *ngIf="moreLikeThis">
|
<a routerLink="/search" [queryParams]="{'more_like': document.id}" class="btn btn-sm btn-outline-secondary" *ngIf="moreLikeThis">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-three-dots" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-three-dots" viewBox="0 0 16 16">
|
||||||
<path fill-rule="evenodd" d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/>
|
<path fill-rule="evenodd" d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>More like this</ng-container>
|
||||||
More like this
|
|
||||||
</a>
|
</a>
|
||||||
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary">
|
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary">
|
||||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>Edit</ng-container>
|
||||||
Edit
|
|
||||||
</a>
|
</a>
|
||||||
<a type="button" class="btn btn-sm btn-outline-secondary" [href]="getPreviewUrl()">
|
<a type="button" class="btn btn-sm btn-outline-secondary" [href]="getPreviewUrl()">
|
||||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-search" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-search" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" d="M10.442 10.442a1 1 0 0 1 1.415 0l3.85 3.85a1 1 0 0 1-1.414 1.415l-3.85-3.85a1 1 0 0 1 0-1.415z"/>
|
<path fill-rule="evenodd" d="M10.442 10.442a1 1 0 0 1 1.415 0l3.85 3.85a1 1 0 0 1-1.414 1.415l-3.85-3.85a1 1 0 0 1 0-1.415z"/>
|
||||||
<path fill-rule="evenodd" d="M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0z"/>
|
<path fill-rule="evenodd" d="M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0z"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>View</ng-container>
|
||||||
View
|
|
||||||
</a>
|
</a>
|
||||||
<a type="button" class="btn btn-sm btn-outline-secondary" [href]="getDownloadUrl()">
|
<a type="button" class="btn btn-sm btn-outline-secondary" [href]="getDownloadUrl()">
|
||||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-download" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-download" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
<path fill-rule="evenodd" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
||||||
<path fill-rule="evenodd" d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
|
<path fill-rule="evenodd" d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>Download</ng-container>
|
||||||
Download
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<small class="text-muted ml-auto">Score:</small>
|
<small class="text-muted ml-auto" i18n>Score:</small>
|
||||||
|
|
||||||
<ngb-progressbar *ngIf="searchScore" [type]="searchScoreClass" [value]="searchScore" class="search-score-bar mx-2" [max]="1"></ngb-progressbar>
|
<ngb-progressbar *ngIf="searchScore" [type]="searchScoreClass" [value]="searchScore" class="search-score-bar mx-2" [max]="1"></ngb-progressbar>
|
||||||
|
|
||||||
<small class="text-muted">Created: {{document.created | date}}</small>
|
<small class="text-muted" i18n>Created: {{document.created | date}}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,15 +12,11 @@ export class DocumentCardLargeComponent implements OnInit {
|
|||||||
|
|
||||||
constructor(private documentService: DocumentService, private sanitizer: DomSanitizer) { }
|
constructor(private documentService: DocumentService, private sanitizer: DomSanitizer) { }
|
||||||
|
|
||||||
_selected = false
|
|
||||||
|
|
||||||
get selected() {
|
|
||||||
return this._selected
|
|
||||||
}
|
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
set selected(value: boolean) {
|
selected = false
|
||||||
this._selected = value
|
|
||||||
|
setSelected(value: boolean) {
|
||||||
|
this.selected = value
|
||||||
this.selectedChange.emit(value)
|
this.selectedChange.emit(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
<div class="col p-2 h-100 document-card">
|
<div class="col p-2 h-100">
|
||||||
<div class="card h-100 shadow-sm" [class.card-selected]="selected">
|
<div class="card h-100 shadow-sm document-card" [class.card-selected]="selected">
|
||||||
<div class="border-bottom" [class.doc-img-background-selected]="selected">
|
<div class="border-bottom" [class.doc-img-background-selected]="selected">
|
||||||
<img class="card-img doc-img" [src]="getThumbUrl()" (click)="selected = !selected">
|
<img class="card-img doc-img" [src]="getThumbUrl()" (click)="setSelected(!selected)">
|
||||||
|
|
||||||
<div style="top: 0; left: 0" class="position-absolute border-right border-bottom bg-light p-1" [class.document-card-check]="!selected">
|
<div class="border-right border-bottom bg-light p-1 rounded document-card-check">
|
||||||
<div class="custom-control custom-checkbox">
|
<div class="custom-control custom-checkbox">
|
||||||
<input type="checkbox" class="custom-control-input" id="smallCardCheck{{document.id}}" [checked]="selected" (change)="selected = $event.target.checked">
|
<input type="checkbox" class="custom-control-input" id="smallCardCheck{{document.id}}" [checked]="selected" (change)="setSelected($event.target.checked)">
|
||||||
<label class="custom-control-label" for="smallCardCheck{{document.id}}"></label>
|
<label class="custom-control-label" for="smallCardCheck{{document.id}}"></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="top: 0; right: 0; font-size: large" class="text-right position-absolute mr-1">
|
<div style="top: 0; right: 0; font-size: large" class="text-right position-absolute mr-1">
|
||||||
<div *ngFor="let t of getTagsLimited$() | async">
|
<div *ngFor="let t of getTagsLimited$() | async">
|
||||||
<app-tag [tag]="t" (click)="clickTag.emit(t.id)" [clickable]="true" linkTitle="Filter by tag"></app-tag>
|
<app-tag [tag]="t" (click)="clickTag.emit(t.id)" [clickable]="true" linkTitle="Filter by tag" i18n-linkTitle></app-tag>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="moreTags">
|
<div *ngIf="moreTags">
|
||||||
<span class="badge badge-secondary">+ {{moreTags}}</span>
|
<span class="badge badge-secondary">+ {{moreTags}}</span>
|
||||||
@ -23,7 +23,7 @@
|
|||||||
<div class="card-body p-2">
|
<div class="card-body p-2">
|
||||||
<p class="card-text">
|
<p class="card-text">
|
||||||
<ng-container *ngIf="document.correspondent">
|
<ng-container *ngIf="document.correspondent">
|
||||||
<a [routerLink]="" title="Filter by correspondent" (click)="clickCorrespondent.emit(document.correspondent)" class="font-weight-bold">{{(document.correspondent$ | async)?.name}}</a>:
|
<a [routerLink]="" title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent)" class="font-weight-bold">{{(document.correspondent$ | async)?.name}}</a>:
|
||||||
</ng-container>
|
</ng-container>
|
||||||
{{document.title | documentTitle}}
|
{{document.title | documentTitle}}
|
||||||
</p>
|
</p>
|
||||||
@ -32,18 +32,18 @@
|
|||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mx-n2">
|
<div class="d-flex justify-content-between align-items-center mx-n2">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" title="Edit">
|
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" title="Edit" i18n-title>
|
||||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a [href]="getPreviewUrl()" class="btn btn-sm btn-outline-secondary" title="View in browser">
|
<a [href]="getPreviewUrl()" class="btn btn-sm btn-outline-secondary" title="View in browser" i18n-title>
|
||||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-search" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-search" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" d="M10.442 10.442a1 1 0 0 1 1.415 0l3.85 3.85a1 1 0 0 1-1.414 1.415l-3.85-3.85a1 1 0 0 1 0-1.415z"/>
|
<path fill-rule="evenodd" d="M10.442 10.442a1 1 0 0 1 1.415 0l3.85 3.85a1 1 0 0 1-1.414 1.415l-3.85-3.85a1 1 0 0 1 0-1.415z"/>
|
||||||
<path fill-rule="evenodd" d="M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0z"/>
|
<path fill-rule="evenodd" d="M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a [href]="getDownloadUrl()" class="btn btn-sm btn-outline-secondary" title="Download">
|
<a [href]="getDownloadUrl()" class="btn btn-sm btn-outline-secondary" title="Download" i18n-title>
|
||||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-download" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-download" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
<path fill-rule="evenodd" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
||||||
<path fill-rule="evenodd" d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
|
<path fill-rule="evenodd" d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
|
||||||
|
@ -8,7 +8,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.document-card-check {
|
.document-card-check {
|
||||||
display: none
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
.custom-control {
|
||||||
|
margin-left: 4px;
|
||||||
|
margin-right: -3px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.document-card:hover .document-card-check {
|
.document-card:hover .document-card-check {
|
||||||
@ -17,8 +25,12 @@
|
|||||||
|
|
||||||
.card-selected {
|
.card-selected {
|
||||||
border-color: $primary;
|
border-color: $primary;
|
||||||
|
|
||||||
|
.document-card-check {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.doc-img-background-selected {
|
.doc-img-background-selected {
|
||||||
background-color: $primaryFaded;
|
background-color: $primaryFaded;
|
||||||
}
|
}
|
||||||
|
@ -12,15 +12,11 @@ export class DocumentCardSmallComponent implements OnInit {
|
|||||||
|
|
||||||
constructor(private documentService: DocumentService) { }
|
constructor(private documentService: DocumentService) { }
|
||||||
|
|
||||||
_selected = false
|
|
||||||
|
|
||||||
get selected() {
|
|
||||||
return this._selected
|
|
||||||
}
|
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
set selected(value: boolean) {
|
selected = false
|
||||||
this._selected = value
|
|
||||||
|
setSelected(value: boolean) {
|
||||||
|
this.selected = value
|
||||||
this.selectedChange.emit(value)
|
this.selectedChange.emit(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,25 +1,16 @@
|
|||||||
<app-page-header [title]="getTitle()">
|
<app-page-header [title]="getTitle()">
|
||||||
|
|
||||||
<div ngbDropdown class="d-inline-block mr-2">
|
<div ngbDropdown class="d-inline-block mr-2">
|
||||||
<button class="btn btn-sm btn-outline-primary" id="dropdownBasic1" ngbDropdownToggle>
|
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
|
||||||
<svg class="toolbaricon" fill="currentColor">
|
<svg class="toolbaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#text-indent-left" />
|
<use xlink:href="assets/bootstrap-icons.svg#text-indent-left" />
|
||||||
</svg>
|
</svg> <ng-container i18n>Select</ng-container>
|
||||||
Bulk edit
|
|
||||||
</button>
|
</button>
|
||||||
<div ngbDropdownMenu aria-labelledby="dropdownBasic1" class="shadow">
|
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
|
||||||
<button ngbDropdownItem (click)="list.selectPage()">Select page</button>
|
<button ngbDropdownItem (click)="list.selectNone()" i18n>Select none</button>
|
||||||
<button ngbDropdownItem (click)="list.selectAll()">Select all</button>
|
<button ngbDropdownItem (click)="list.selectPage()" i18n>Select page</button>
|
||||||
<button ngbDropdownItem (click)="list.selectNone()">Select none</button>
|
<button ngbDropdownItem (click)="list.selectAll()" i18n>Select all</button>
|
||||||
<div class="dropdown-divider"></div>
|
|
||||||
<button ngbDropdownItem [disabled]="list.selected.size == 0" (click)="bulkSetCorrespondent()">Set correspondent</button>
|
|
||||||
<button ngbDropdownItem [disabled]="list.selected.size == 0" (click)="bulkRemoveCorrespondent()">Remove correspondent</button>
|
|
||||||
<button ngbDropdownItem [disabled]="list.selected.size == 0" (click)="bulkSetDocumentType()">Set document type</button>
|
|
||||||
<button ngbDropdownItem [disabled]="list.selected.size == 0" (click)="bulkRemoveDocumentType()">Remove document type</button>
|
|
||||||
<button ngbDropdownItem [disabled]="list.selected.size == 0" (click)="bulkAddTag()">Add tag</button>
|
|
||||||
<button ngbDropdownItem [disabled]="list.selected.size == 0" (click)="bulkRemoveTag()">Remove tag</button>
|
|
||||||
<div class="dropdown-divider"></div>
|
|
||||||
<button ngbDropdownItem [disabled]="list.selected.size == 0" (click)="bulkDelete()">Delete</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -47,7 +38,7 @@
|
|||||||
|
|
||||||
<div class="btn-group btn-group-toggle ml-2" ngbRadioGroup [(ngModel)]="list.sortReverse">
|
<div class="btn-group btn-group-toggle ml-2" ngbRadioGroup [(ngModel)]="list.sortReverse">
|
||||||
<div ngbDropdown class="btn-group">
|
<div ngbDropdown class="btn-group">
|
||||||
<button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle>Sort by</button>
|
<button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle i18n>Sort by</button>
|
||||||
<div ngbDropdownMenu aria-labelledby="dropdownBasic1" class="shadow">
|
<div ngbDropdownMenu aria-labelledby="dropdownBasic1" class="shadow">
|
||||||
<button *ngFor="let f of getSortFields()" ngbDropdownItem (click)="list.sortField = f.field"
|
<button *ngFor="let f of getSortFields()" ngbDropdownItem (click)="list.sortField = f.field"
|
||||||
[class.active]="list.sortField == f.field">{{f.name}}</button>
|
[class.active]="list.sortField == f.field">{{f.name}}</button>
|
||||||
@ -70,15 +61,15 @@
|
|||||||
<div class="btn-group ml-2">
|
<div class="btn-group ml-2">
|
||||||
|
|
||||||
<div class="btn-group" ngbDropdown role="group">
|
<div class="btn-group" ngbDropdown role="group">
|
||||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle" ngbDropdownToggle>Views</button>
|
<button class="btn btn-sm btn-outline-primary dropdown-toggle" ngbDropdownToggle i18n>Views</button>
|
||||||
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
||||||
<ng-container *ngIf="!list.savedViewId">
|
<ng-container *ngIf="!list.savedViewId">
|
||||||
<button ngbDropdownItem *ngFor="let view of savedViewService.allViews" (click)="loadViewConfig(view)">{{view.name}}</button>
|
<button ngbDropdownItem *ngFor="let view of savedViewService.allViews" (click)="loadViewConfig(view)">{{view.name}}</button>
|
||||||
<div class="dropdown-divider" *ngIf="savedViewService.allViews.length > 0"></div>
|
<div class="dropdown-divider" *ngIf="savedViewService.allViews.length > 0"></div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.savedViewId">Save "{{list.savedViewTitle}}"</button>
|
<button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.savedViewId" i18n>Save "{{list.savedViewTitle}}"</button>
|
||||||
<button ngbDropdownItem (click)="saveViewConfigAs()">Save as...</button>
|
<button ngbDropdownItem (click)="saveViewConfigAs()" i18n>Save as...</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -87,11 +78,13 @@
|
|||||||
</app-page-header>
|
</app-page-header>
|
||||||
|
|
||||||
<div class="w-100 mb-2 mb-sm-4">
|
<div class="w-100 mb-2 mb-sm-4">
|
||||||
<app-filter-editor [(filterRules)]="list.filterRules" #filterEditor></app-filter-editor>
|
<app-filter-editor [hidden]="isBulkEditing" [(filterRules)]="list.filterRules" #filterEditor></app-filter-editor>
|
||||||
|
<app-bulk-editor [hidden]="!isBulkEditing"></app-bulk-editor>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<p><span *ngIf="list.selected.size > 0">Selected {{list.selected.size}} of </span>{{list.collectionSize || 0}} document(s) <span *ngIf="isFiltered">(filtered)</span></p>
|
<p i18n *ngIf="list.selected.size > 0">Selected {{list.selected.size}} of {{list.collectionSize || 0}} {list.collectionSize, plural, =1 {document} other {documents}}</p>
|
||||||
|
<p *ngIf="list.selected.size == 0">{list.collectionSize, plural, =1 {1 document} other {{{list.collectionSize || 0}} documents}}</p>
|
||||||
<ngb-pagination [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
|
<ngb-pagination [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
|
||||||
[rotate]="true" (pageChange)="list.reload()" aria-label="Default pagination"></ngb-pagination>
|
[rotate]="true" (pageChange)="list.reload()" aria-label="Default pagination"></ngb-pagination>
|
||||||
</div>
|
</div>
|
||||||
@ -104,12 +97,12 @@
|
|||||||
<table class="table table-sm border shadow-sm" *ngIf="displayMode == 'details'">
|
<table class="table table-sm border shadow-sm" *ngIf="displayMode == 'details'">
|
||||||
<thead>
|
<thead>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th class="d-none d-lg-table-cell">ASN</th>
|
<th class="d-none d-lg-table-cell" i18n>ASN</th>
|
||||||
<th class="d-none d-md-table-cell">Correspondent</th>
|
<th class="d-none d-md-table-cell" i18n>Correspondent</th>
|
||||||
<th>Title</th>
|
<th i18n>Title</th>
|
||||||
<th class="d-none d-xl-table-cell">Document type</th>
|
<th class="d-none d-xl-table-cell" i18n>Document type</th>
|
||||||
<th>Created</th>
|
<th i18n>Created</th>
|
||||||
<th class="d-none d-xl-table-cell">Added</th>
|
<th class="d-none d-xl-table-cell" i18n>Added</th>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let d of list.documents; trackBy: trackByDocumentId" [ngClass]="list.isSelected(d) ? 'table-row-selected' : ''">
|
<tr *ngFor="let d of list.documents; trackBy: trackByDocumentId" [ngClass]="list.isSelected(d) ? 'table-row-selected' : ''">
|
||||||
@ -146,7 +139,6 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
||||||
<div class="m-n2 row row-cols-paperless-cards" *ngIf="displayMode == 'smallCards'">
|
<div class="m-n2 row row-cols-paperless-cards" *ngIf="displayMode == 'smallCards'">
|
||||||
<app-document-card-small [selected]="list.isSelected(d)" (selectedChange)="list.setSelected(d, $event)" [document]="d" *ngFor="let d of list.documents; trackBy: trackByDocumentId" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)"></app-document-card-small>
|
<app-document-card-small [selected]="list.isSelected(d)" (selectedChange)="list.setSelected(d, $event)" [document]="d" *ngFor="let d of list.documents; trackBy: trackByDocumentId" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)"></app-document-card-small>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,22 +1,14 @@
|
|||||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { tap } from 'rxjs/operators';
|
|
||||||
import { PaperlessDocument } from 'src/app/data/paperless-document';
|
import { PaperlessDocument } from 'src/app/data/paperless-document';
|
||||||
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
|
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
|
||||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
||||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
|
import { DOCUMENT_SORT_FIELDS } from 'src/app/services/rest/document.service';
|
||||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service';
|
|
||||||
import { DocumentService, DOCUMENT_SORT_FIELDS } from 'src/app/services/rest/document.service';
|
|
||||||
import { TagService } from 'src/app/services/rest/tag.service';
|
|
||||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
|
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
|
||||||
import { Toast, ToastService } from 'src/app/services/toast.service';
|
import { Toast, ToastService } from 'src/app/services/toast.service';
|
||||||
import { FilterEditorComponent } from '../filter-editor/filter-editor.component';
|
import { FilterEditorComponent } from './filter-editor/filter-editor.component';
|
||||||
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component';
|
|
||||||
import { SelectDialogComponent } from '../common/select-dialog/select-dialog.component';
|
|
||||||
import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component';
|
import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component';
|
||||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-document-list',
|
selector: 'app-document-list',
|
||||||
@ -31,12 +23,7 @@ export class DocumentListComponent implements OnInit {
|
|||||||
public route: ActivatedRoute,
|
public route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
public modalService: NgbModal,
|
private modalService: NgbModal) { }
|
||||||
private correspondentService: CorrespondentService,
|
|
||||||
private documentTypeService: DocumentTypeService,
|
|
||||||
private tagService: TagService,
|
|
||||||
private documentService: DocumentService,
|
|
||||||
private openDocumentService: OpenDocumentsService) { }
|
|
||||||
|
|
||||||
@ViewChild("filterEditor")
|
@ViewChild("filterEditor")
|
||||||
private filterEditor: FilterEditorComponent
|
private filterEditor: FilterEditorComponent
|
||||||
@ -48,13 +35,17 @@ export class DocumentListComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getTitle() {
|
getTitle() {
|
||||||
return this.list.savedViewTitle || "Documents"
|
return this.list.savedViewTitle || $localize`Documents`
|
||||||
}
|
}
|
||||||
|
|
||||||
getSortFields() {
|
getSortFields() {
|
||||||
return DOCUMENT_SORT_FIELDS
|
return DOCUMENT_SORT_FIELDS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isBulkEditing(): boolean {
|
||||||
|
return this.list.selected.size > 0
|
||||||
|
}
|
||||||
|
|
||||||
saveDisplayMode() {
|
saveDisplayMode() {
|
||||||
localStorage.setItem('document-list:displayMode', this.displayMode)
|
localStorage.setItem('document-list:displayMode', this.displayMode)
|
||||||
}
|
}
|
||||||
@ -90,7 +81,7 @@ export class DocumentListComponent implements OnInit {
|
|||||||
|
|
||||||
saveViewConfig() {
|
saveViewConfig() {
|
||||||
this.savedViewService.update(this.list.savedView).subscribe(result => {
|
this.savedViewService.update(this.list.savedView).subscribe(result => {
|
||||||
this.toastService.showToast(Toast.make("Information", `View "${this.list.savedView.name}" saved successfully.`))
|
this.toastService.showInfo($localize`View "${this.list.savedView.name}" saved successfully.`)
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -109,139 +100,33 @@ export class DocumentListComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
this.savedViewService.create(savedView).subscribe(() => {
|
this.savedViewService.create(savedView).subscribe(() => {
|
||||||
modal.close()
|
modal.close()
|
||||||
this.toastService.showToast(Toast.make("Information", `View "${savedView.name}" created successfully.`))
|
this.toastService.showInfo($localize`View "${savedView.name}" created successfully.`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
clickTag(tagID: number) {
|
clickTag(tagID: number) {
|
||||||
this.filterEditor.toggleTag(tagID)
|
this.list.selectNone()
|
||||||
|
setTimeout(() => {
|
||||||
|
this.filterEditor.toggleTag(tagID)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
clickCorrespondent(correspondentID: number) {
|
clickCorrespondent(correspondentID: number) {
|
||||||
this.filterEditor.toggleCorrespondent(correspondentID)
|
this.list.selectNone()
|
||||||
|
setTimeout(() => {
|
||||||
|
this.filterEditor.toggleCorrespondent(correspondentID)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
clickDocumentType(documentTypeID: number) {
|
clickDocumentType(documentTypeID: number) {
|
||||||
this.filterEditor.toggleDocumentType(documentTypeID)
|
this.list.selectNone()
|
||||||
|
setTimeout(() => {
|
||||||
|
this.filterEditor.toggleDocumentType(documentTypeID)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
trackByDocumentId(index, item: PaperlessDocument) {
|
trackByDocumentId(index, item: PaperlessDocument) {
|
||||||
return item.id
|
return item.id
|
||||||
}
|
}
|
||||||
|
|
||||||
private executeBulkOperation(method: string, args): Observable<any> {
|
|
||||||
return this.documentService.bulkEdit(Array.from(this.list.selected), method, args).pipe(
|
|
||||||
tap(() => {
|
|
||||||
this.list.reload()
|
|
||||||
this.list.selected.forEach(id => {
|
|
||||||
this.openDocumentService.refreshDocument(id)
|
|
||||||
})
|
|
||||||
this.list.selectNone()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
bulkSetCorrespondent() {
|
|
||||||
let modal = this.modalService.open(SelectDialogComponent, {backdrop: 'static'})
|
|
||||||
modal.componentInstance.title = "Select correspondent"
|
|
||||||
modal.componentInstance.message = `Select the correspondent you wish to assign to ${this.list.selected.size} selected document(s):`
|
|
||||||
this.correspondentService.listAll().subscribe(response => {
|
|
||||||
modal.componentInstance.objects = response.results
|
|
||||||
})
|
|
||||||
modal.componentInstance.selectClicked.subscribe(selectedId => {
|
|
||||||
this.executeBulkOperation('set_correspondent', {"correspondent": selectedId}).subscribe(
|
|
||||||
response => {
|
|
||||||
modal.close()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
bulkRemoveCorrespondent() {
|
|
||||||
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
|
||||||
modal.componentInstance.title = "Remove correspondent"
|
|
||||||
modal.componentInstance.message = `This operation will remove the correspondent from all ${this.list.selected.size} selected document(s).`
|
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
|
||||||
this.executeBulkOperation('set_correspondent', {"correspondent": null}).subscribe(r => {
|
|
||||||
modal.close()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
bulkSetDocumentType() {
|
|
||||||
let modal = this.modalService.open(SelectDialogComponent, {backdrop: 'static'})
|
|
||||||
modal.componentInstance.title = "Select document type"
|
|
||||||
modal.componentInstance.message = `Select the document type you wish to assign to ${this.list.selected.size} selected document(s):`
|
|
||||||
this.documentTypeService.listAll().subscribe(response => {
|
|
||||||
modal.componentInstance.objects = response.results
|
|
||||||
})
|
|
||||||
modal.componentInstance.selectClicked.subscribe(selectedId => {
|
|
||||||
this.executeBulkOperation('set_document_type', {"document_type": selectedId}).subscribe(
|
|
||||||
response => {
|
|
||||||
modal.close()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
bulkRemoveDocumentType() {
|
|
||||||
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
|
||||||
modal.componentInstance.title = "Remove document type"
|
|
||||||
modal.componentInstance.message = `This operation will remove the document type from all ${this.list.selected.size} selected document(s).`
|
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
|
||||||
this.executeBulkOperation('set_document_type', {"document_type": null}).subscribe(r => {
|
|
||||||
modal.close()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
bulkAddTag() {
|
|
||||||
let modal = this.modalService.open(SelectDialogComponent, {backdrop: 'static'})
|
|
||||||
modal.componentInstance.title = "Select tag"
|
|
||||||
modal.componentInstance.message = `Select the tag you wish to assign to ${this.list.selected.size} selected document(s):`
|
|
||||||
this.tagService.listAll().subscribe(response => {
|
|
||||||
modal.componentInstance.objects = response.results
|
|
||||||
})
|
|
||||||
modal.componentInstance.selectClicked.subscribe(selectedId => {
|
|
||||||
this.executeBulkOperation('add_tag', {"tag": selectedId}).subscribe(
|
|
||||||
response => {
|
|
||||||
modal.close()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
bulkRemoveTag() {
|
|
||||||
let modal = this.modalService.open(SelectDialogComponent, {backdrop: 'static'})
|
|
||||||
modal.componentInstance.title = "Select tag"
|
|
||||||
modal.componentInstance.message = `Select the tag you wish to remove from ${this.list.selected.size} selected document(s):`
|
|
||||||
this.tagService.listAll().subscribe(response => {
|
|
||||||
modal.componentInstance.objects = response.results
|
|
||||||
})
|
|
||||||
modal.componentInstance.selectClicked.subscribe(selectedId => {
|
|
||||||
this.executeBulkOperation('remove_tag', {"tag": selectedId}).subscribe(
|
|
||||||
response => {
|
|
||||||
modal.close()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
bulkDelete() {
|
|
||||||
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
|
||||||
modal.componentInstance.delayConfirm(5)
|
|
||||||
modal.componentInstance.title = "Delete confirm"
|
|
||||||
modal.componentInstance.messageBold = `This operation will permanently delete all ${this.list.selected.size} selected document(s).`
|
|
||||||
modal.componentInstance.message = `This operation cannot be undone.`
|
|
||||||
modal.componentInstance.btnClass = "btn-danger"
|
|
||||||
modal.componentInstance.btnCaption = "Delete document(s)"
|
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
|
||||||
this.executeBulkOperation("delete", {}).subscribe(
|
|
||||||
response => {
|
|
||||||
modal.close()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,51 @@
|
|||||||
|
<div class="row">
|
||||||
|
<div class="col mb-2 mb-xl-0">
|
||||||
|
<div class="form-inline d-flex">
|
||||||
|
<label class="text-muted mr-2" i18n>Filter by:</label>
|
||||||
|
<input class="form-control form-control-sm flex-grow-1" type="text" [(ngModel)]="titleFilter" placeholder="Title" i18n-placeholder>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-100 d-xl-none"></div>
|
||||||
|
<div class="col col-xl-auto mb-2 mb-xl-0">
|
||||||
|
<div class="d-flex">
|
||||||
|
<app-filterable-dropdown class="mr-2 mr-md-3" title="Tags" icon="tag-fill" i18n-title
|
||||||
|
filterPlaceholder="Filter tags" i18n-filterPlaceholder
|
||||||
|
[items]="tags"
|
||||||
|
[(selectionModel)]="tagSelectionModel"
|
||||||
|
(selectionModelChange)="updateRules()"
|
||||||
|
[multiple]="true"
|
||||||
|
[allowSelectNone]="true"></app-filterable-dropdown>
|
||||||
|
<app-filterable-dropdown class="mr-2 mr-md-3" title="Correspondent" icon="person-fill" i18n-title
|
||||||
|
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
|
||||||
|
[items]="correspondents"
|
||||||
|
[(selectionModel)]="correspondentSelectionModel"
|
||||||
|
(selectionModelChange)="updateRules()"
|
||||||
|
[allowSelectNone]="true"></app-filterable-dropdown>
|
||||||
|
<app-filterable-dropdown class="mr-2 mr-md-3" title="Document type" icon="file-earmark-fill" i18n-title
|
||||||
|
filterPlaceholder="Filter document types" i18n-filterPlaceholder
|
||||||
|
[items]="documentTypes"
|
||||||
|
[(selectionModel)]="documentTypeSelectionModel"
|
||||||
|
(selectionModelChange)="updateRules()"
|
||||||
|
[allowSelectNone]="true"></app-filterable-dropdown>
|
||||||
|
<app-date-dropdown class="mr-2 mr-md-3"
|
||||||
|
title="Created" i18n-title
|
||||||
|
(datesSet)="updateRules()"
|
||||||
|
[(dateBefore)]="dateCreatedBefore"
|
||||||
|
[(dateAfter)]="dateCreatedAfter"></app-date-dropdown>
|
||||||
|
<app-date-dropdown
|
||||||
|
[(dateBefore)]="dateAddedBefore"
|
||||||
|
[(dateAfter)]="dateAddedAfter"
|
||||||
|
title="Added" i18n-title
|
||||||
|
(datesSet)="updateRules()"></app-date-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-100 d-xl-none"></div>
|
||||||
|
<div class="col col-xl-auto mb-2 mb-xl-0">
|
||||||
|
<button class="btn btn-link btn-sm px-0 mx-0 ml-xl-n4" [disabled]="!hasFilters()" (click)="clearSelected()">
|
||||||
|
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
||||||
|
</svg> <ng-container i18n>Clear all filters</ng-container>
|
||||||
|
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,203 @@
|
|||||||
|
import { Component, EventEmitter, Input, Output, OnInit, OnDestroy } from '@angular/core';
|
||||||
|
import { PaperlessTag } from 'src/app/data/paperless-tag';
|
||||||
|
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
|
||||||
|
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
|
||||||
|
import { Subject, Subscription } from 'rxjs';
|
||||||
|
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
||||||
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service';
|
||||||
|
import { TagService } from 'src/app/services/rest/tag.service';
|
||||||
|
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
|
||||||
|
import { FilterRule } from 'src/app/data/filter-rule';
|
||||||
|
import { FILTER_ADDED_AFTER, FILTER_ADDED_BEFORE, FILTER_CORRESPONDENT, FILTER_CREATED_AFTER, FILTER_CREATED_BEFORE, FILTER_DOCUMENT_TYPE, FILTER_HAS_ANY_TAG, FILTER_HAS_TAG, FILTER_TITLE } from 'src/app/data/filter-rule-type';
|
||||||
|
import { FilterableDropdownSelectionModel } from '../../common/filterable-dropdown/filterable-dropdown.component';
|
||||||
|
import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-filter-editor',
|
||||||
|
templateUrl: './filter-editor.component.html',
|
||||||
|
styleUrls: ['./filter-editor.component.scss']
|
||||||
|
})
|
||||||
|
export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
generateFilterName() {
|
||||||
|
if (this.filterRules.length == 1) {
|
||||||
|
let rule = this.filterRules[0]
|
||||||
|
switch(this.filterRules[0].rule_type) {
|
||||||
|
|
||||||
|
case FILTER_CORRESPONDENT:
|
||||||
|
return $localize`Correspondent: ${this.correspondents.find(c => c.id == +rule.value)?.name}`
|
||||||
|
|
||||||
|
case FILTER_DOCUMENT_TYPE:
|
||||||
|
return $localize`Type: ${this.documentTypes.find(dt => dt.id == +rule.value)?.name}`
|
||||||
|
|
||||||
|
case FILTER_HAS_TAG:
|
||||||
|
return $localize`Tag: ${this.tags.find(t => t.id == +rule.value)?.name}`
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private documentTypeService: DocumentTypeService,
|
||||||
|
private tagService: TagService,
|
||||||
|
private correspondentService: CorrespondentService
|
||||||
|
) { }
|
||||||
|
|
||||||
|
tags: PaperlessTag[] = []
|
||||||
|
correspondents: PaperlessCorrespondent[] = []
|
||||||
|
documentTypes: PaperlessDocumentType[] = []
|
||||||
|
|
||||||
|
_titleFilter = ""
|
||||||
|
|
||||||
|
tagSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
|
correspondentSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
|
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
|
|
||||||
|
dateCreatedBefore: string
|
||||||
|
dateCreatedAfter: string
|
||||||
|
dateAddedBefore: string
|
||||||
|
dateAddedAfter: string
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set filterRules (value: FilterRule[]) {
|
||||||
|
this.documentTypeSelectionModel.clear(false)
|
||||||
|
this.tagSelectionModel.clear(false)
|
||||||
|
this.correspondentSelectionModel.clear(false)
|
||||||
|
|
||||||
|
value.forEach(rule => {
|
||||||
|
switch (rule.rule_type) {
|
||||||
|
case FILTER_TITLE:
|
||||||
|
this._titleFilter = rule.value
|
||||||
|
break
|
||||||
|
case FILTER_CREATED_AFTER:
|
||||||
|
this.dateCreatedAfter = rule.value
|
||||||
|
break
|
||||||
|
case FILTER_CREATED_BEFORE:
|
||||||
|
this.dateCreatedBefore = rule.value
|
||||||
|
break
|
||||||
|
case FILTER_ADDED_AFTER:
|
||||||
|
this.dateAddedAfter = rule.value
|
||||||
|
break
|
||||||
|
case FILTER_ADDED_BEFORE:
|
||||||
|
this.dateAddedBefore = rule.value
|
||||||
|
break
|
||||||
|
case FILTER_HAS_TAG:
|
||||||
|
this.tagSelectionModel.set(rule.value ? +rule.value : null, ToggleableItemState.Selected, false)
|
||||||
|
break
|
||||||
|
case FILTER_HAS_ANY_TAG:
|
||||||
|
this.tagSelectionModel.set(null, ToggleableItemState.Selected, false)
|
||||||
|
break
|
||||||
|
case FILTER_CORRESPONDENT:
|
||||||
|
this.correspondentSelectionModel.set(rule.value ? +rule.value : null, ToggleableItemState.Selected, false)
|
||||||
|
break
|
||||||
|
case FILTER_DOCUMENT_TYPE:
|
||||||
|
this.documentTypeSelectionModel.set(rule.value ? +rule.value : null, ToggleableItemState.Selected, false)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get filterRules() {
|
||||||
|
let filterRules: FilterRule[] = []
|
||||||
|
if (this._titleFilter) {
|
||||||
|
filterRules.push({rule_type: FILTER_TITLE, value: this._titleFilter})
|
||||||
|
}
|
||||||
|
if (this.tagSelectionModel.isNoneSelected()) {
|
||||||
|
filterRules.push({rule_type: FILTER_HAS_ANY_TAG, value: "false"})
|
||||||
|
} else {
|
||||||
|
this.tagSelectionModel.getSelectedItems().filter(tag => tag.id).forEach(tag => {
|
||||||
|
filterRules.push({rule_type: FILTER_HAS_TAG, value: tag.id?.toString()})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.correspondentSelectionModel.getSelectedItems().forEach(correspondent => {
|
||||||
|
filterRules.push({rule_type: FILTER_CORRESPONDENT, value: correspondent.id?.toString()})
|
||||||
|
})
|
||||||
|
this.documentTypeSelectionModel.getSelectedItems().forEach(documentType => {
|
||||||
|
filterRules.push({rule_type: FILTER_DOCUMENT_TYPE, value: documentType.id?.toString()})
|
||||||
|
})
|
||||||
|
if (this.dateCreatedBefore) {
|
||||||
|
filterRules.push({rule_type: FILTER_CREATED_BEFORE, value: this.dateCreatedBefore})
|
||||||
|
}
|
||||||
|
if (this.dateCreatedAfter) {
|
||||||
|
filterRules.push({rule_type: FILTER_CREATED_AFTER, value: this.dateCreatedAfter})
|
||||||
|
}
|
||||||
|
if (this.dateAddedBefore) {
|
||||||
|
filterRules.push({rule_type: FILTER_ADDED_BEFORE, value: this.dateAddedBefore})
|
||||||
|
}
|
||||||
|
if (this.dateAddedAfter) {
|
||||||
|
filterRules.push({rule_type: FILTER_ADDED_AFTER, value: this.dateAddedAfter})
|
||||||
|
}
|
||||||
|
return filterRules
|
||||||
|
}
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
filterRulesChange = new EventEmitter<FilterRule[]>()
|
||||||
|
|
||||||
|
updateRules() {
|
||||||
|
this.filterRulesChange.next(this.filterRules)
|
||||||
|
}
|
||||||
|
|
||||||
|
hasFilters() {
|
||||||
|
return this._titleFilter ||
|
||||||
|
this.dateAddedAfter || this.dateAddedBefore || this.dateCreatedAfter || this.dateCreatedBefore ||
|
||||||
|
this.tagSelectionModel.selectionSize() || this.correspondentSelectionModel.selectionSize() || this.documentTypeSelectionModel.selectionSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
get titleFilter() {
|
||||||
|
return this._titleFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
set titleFilter(value) {
|
||||||
|
this.titleFilterDebounce.next(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
titleFilterDebounce: Subject<string>
|
||||||
|
subscription: Subscription
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.tagService.listAll().subscribe(result => this.tags = result.results)
|
||||||
|
this.correspondentService.listAll().subscribe(result => this.correspondents = result.results)
|
||||||
|
this.documentTypeService.listAll().subscribe(result => this.documentTypes = result.results)
|
||||||
|
|
||||||
|
this.titleFilterDebounce = new Subject<string>()
|
||||||
|
|
||||||
|
this.subscription = this.titleFilterDebounce.pipe(
|
||||||
|
debounceTime(400),
|
||||||
|
distinctUntilChanged()
|
||||||
|
).subscribe(title => {
|
||||||
|
this._titleFilter = title
|
||||||
|
this.updateRules()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.titleFilterDebounce.complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSelected() {
|
||||||
|
this._titleFilter = ""
|
||||||
|
this.tagSelectionModel.clear(false)
|
||||||
|
this.documentTypeSelectionModel.clear(false)
|
||||||
|
this.correspondentSelectionModel.clear(false)
|
||||||
|
this.dateAddedBefore = null
|
||||||
|
this.dateAddedAfter = null
|
||||||
|
this.dateCreatedBefore = null
|
||||||
|
this.dateCreatedAfter = null
|
||||||
|
this.updateRules()
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleTag(tagId: number) {
|
||||||
|
this.tagSelectionModel.toggle(tagId)
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleCorrespondent(correspondentId: number) {
|
||||||
|
this.correspondentSelectionModel.toggle(correspondentId)
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleDocumentType(documentTypeId: number) {
|
||||||
|
this.documentTypeSelectionModel.toggle(documentTypeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,17 +1,17 @@
|
|||||||
<form [formGroup]="saveViewConfigForm" class="needs-validation" novalidate (ngSubmit)="save()">
|
<form [formGroup]="saveViewConfigForm" class="needs-validation" novalidate (ngSubmit)="save()">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">Save current view</h4>
|
<h4 class="modal-title" id="modal-basic-title" i18n>Save current view</h4>
|
||||||
<button type="button" class="close" aria-label="Close" (click)="cancel()">
|
<button type="button" class="close" aria-label="Close" (click)="cancel()">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<app-input-text title="Name" formControlName="name"></app-input-text>
|
<app-input-text i18n-title title="Name" formControlName="name"></app-input-text>
|
||||||
<app-input-check title="Show in side bar" formControlName="showInSideBar"></app-input-check>
|
<app-input-check i18n-title title="Show in sidebar" formControlName="showInSideBar"></app-input-check>
|
||||||
<app-input-check title="Show on dashboard" formControlName="showOnDashboard"></app-input-check>
|
<app-input-check i18n-title title="Show on dashboard" formControlName="showOnDashboard"></app-input-check>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button>
|
<button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n>Cancel</button>
|
||||||
<button type="submit" class="btn btn-primary">Save</button>
|
<button type="submit" class="btn btn-primary" i18n>Save</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-left-0 border-right-0 border-bottom" role="menuitem" (click)="toggleItem()">
|
|
||||||
<div class="selected-icon mr-1">
|
|
||||||
<svg *ngIf="selected" width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-check" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" d="M10.97 4.97a.75.75 0 0 1 1.071 1.05l-3.992 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.236.236 0 0 1 .02-.022z"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="mr-1">
|
|
||||||
<app-tag *ngIf="isTag; else displayName" [tag]="item" [clickable]="true" linkTitle="Filter by tag"></app-tag>
|
|
||||||
<ng-template #displayName><small>{{item.name}}</small></ng-template>
|
|
||||||
</div>
|
|
||||||
<div class="badge badge-light rounded-pill ml-auto mr-1">{{item.document_count}}</div>
|
|
||||||
</button>
|
|
@ -1,25 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { FilterDropodownButtonComponent } from './filter-dropdown-button.component';
|
|
||||||
|
|
||||||
describe('FilterDropodownButtonComponent', () => {
|
|
||||||
let component: FilterDropodownButtonComponent;
|
|
||||||
let fixture: ComponentFixture<FilterDropodownButtonComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
declarations: [ FilterDropodownButtonComponent ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(FilterDropodownButtonComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,32 +0,0 @@
|
|||||||
import { Component, EventEmitter, Input, Output, OnInit } from '@angular/core';
|
|
||||||
import { PaperlessTag } from 'src/app/data/paperless-tag';
|
|
||||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
|
|
||||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-filter-dropdown-button',
|
|
||||||
templateUrl: './filter-dropdown-button.component.html',
|
|
||||||
styleUrls: ['./filter-dropdown-button.component.scss']
|
|
||||||
})
|
|
||||||
export class FilterDropdownButtonComponent implements OnInit {
|
|
||||||
|
|
||||||
@Input()
|
|
||||||
item: PaperlessTag | PaperlessDocumentType | PaperlessCorrespondent
|
|
||||||
|
|
||||||
@Input()
|
|
||||||
selected: boolean
|
|
||||||
|
|
||||||
@Output()
|
|
||||||
toggle = new EventEmitter()
|
|
||||||
|
|
||||||
isTag: boolean
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.isTag = 'is_inbox_tag' in this.item // ~ this.item instanceof PaperlessTag
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleItem(): void {
|
|
||||||
this.selected = !this.selected
|
|
||||||
this.toggle.emit(this.item)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
<div class="btn-group" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #filterDropdown="ngbDropdown">
|
|
||||||
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="itemsSelected?.length > 0 ? 'btn-primary' : 'btn-outline-primary'">
|
|
||||||
<div class="d-none d-md-inline">{{title}}</div>
|
|
||||||
<div class="d-inline-block d-md-none">
|
|
||||||
<svg class="toolbaricon" fill="currentColor">
|
|
||||||
<use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<ng-container *ngIf="itemsSelected?.length > 0">
|
|
||||||
<div class="badge bg-secondary text-light rounded-pill badge-corner">
|
|
||||||
{{itemsSelected?.length}}
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
</button>
|
|
||||||
<div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
|
|
||||||
<div class="list-group list-group-flush">
|
|
||||||
<div class="list-group-item">
|
|
||||||
<div class="input-group input-group-sm">
|
|
||||||
<input class="form-control" type="text" [(ngModel)]="filterText" placeholder="Filter {{title}}" (keyup.enter)="listFilterEnter()" #listFilterTextInput>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div *ngIf="items" class="items">
|
|
||||||
<ng-container *ngFor="let item of items | filter: filterText; let i = index">
|
|
||||||
<app-filter-dropdown-button [item]="item" [selected]="isItemSelected(item)" (toggle)="toggleItem($event)"></app-filter-dropdown-button>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,58 +0,0 @@
|
|||||||
import { Component, EventEmitter, Input, Output, ElementRef, ViewChild } from '@angular/core';
|
|
||||||
import { ObjectWithId } from 'src/app/data/object-with-id';
|
|
||||||
import { FilterPipe } from 'src/app/pipes/filter.pipe';
|
|
||||||
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-filter-dropdown',
|
|
||||||
templateUrl: './filter-dropdown.component.html',
|
|
||||||
styleUrls: ['./filter-dropdown.component.scss']
|
|
||||||
})
|
|
||||||
export class FilterDropdownComponent {
|
|
||||||
|
|
||||||
constructor(private filterPipe: FilterPipe) { }
|
|
||||||
|
|
||||||
@Input()
|
|
||||||
items: ObjectWithId[]
|
|
||||||
|
|
||||||
@Input()
|
|
||||||
itemsSelected: ObjectWithId[]
|
|
||||||
|
|
||||||
@Input()
|
|
||||||
title: string
|
|
||||||
|
|
||||||
@Input()
|
|
||||||
icon: string
|
|
||||||
|
|
||||||
@Output()
|
|
||||||
toggle = new EventEmitter()
|
|
||||||
|
|
||||||
@ViewChild('listFilterTextInput') listFilterTextInput: ElementRef
|
|
||||||
@ViewChild('filterDropdown') filterDropdown: NgbDropdown
|
|
||||||
|
|
||||||
filterText: string
|
|
||||||
|
|
||||||
toggleItem(item: ObjectWithId): void {
|
|
||||||
this.toggle.emit(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
isItemSelected(item: ObjectWithId): boolean {
|
|
||||||
return this.itemsSelected?.find(i => i.id == item.id) !== undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
dropdownOpenChange(open: boolean): void {
|
|
||||||
if (open) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.listFilterTextInput.nativeElement.focus();
|
|
||||||
}, 0);
|
|
||||||
} else {
|
|
||||||
this.filterText = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
listFilterEnter(): void {
|
|
||||||
let filtered = this.filterPipe.transform(this.items, this.filterText)
|
|
||||||
if (filtered.length == 1) this.toggleItem(filtered.shift())
|
|
||||||
this.filterDropdown.close()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
<div class="row">
|
|
||||||
<div class="col mb-2 mb-xl-0">
|
|
||||||
<div class="form-inline d-flex">
|
|
||||||
<label class="text-muted mr-2">Filter by:</label>
|
|
||||||
<input class="form-control form-control-sm flex-grow-1" type="text" [(ngModel)]="titleFilter" placeholder="Title">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="w-100 d-xl-none"></div>
|
|
||||||
<div class="col col-xl-auto mb-2 mb-xl-0">
|
|
||||||
<div class="d-flex">
|
|
||||||
<app-filter-dropdown class="mr-2 mr-md-3" [items]="tags" [itemsSelected]="selectedTags" title="Tags" icon="tag-fill" (toggle)="toggleTag($event.id)"></app-filter-dropdown>
|
|
||||||
<app-filter-dropdown class="mr-2 mr-md-3" [items]="correspondents" [itemsSelected]="selectedCorrespondents" title="Correspondents" icon="person-fill" (toggle)="toggleCorrespondent($event.id)"></app-filter-dropdown>
|
|
||||||
<app-filter-dropdown class="mr-2 mr-md-3" [items]="documentTypes" [itemsSelected]="selectedDocumentTypes" title="Document types" icon="file-earmark-fill" (toggle)="toggleDocumentType($event.id)"></app-filter-dropdown>
|
|
||||||
<app-filter-dropdown-date class="mr-2 mr-md-3" [dateBefore]="dateCreatedBefore" [dateAfter]="dateCreatedAfter" title="Created" (datesSet)="onDatesCreatedSet($event)"></app-filter-dropdown-date>
|
|
||||||
<app-filter-dropdown-date [dateBefore]="dateAddedBefore" [dateAfter]="dateAddedAfter" title="Added" (datesSet)="onDatesAddedSet($event)"></app-filter-dropdown-date>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="w-100 d-xl-none"></div>
|
|
||||||
<div class="col col-xl-auto mb-2 mb-xl-0">
|
|
||||||
<button class="btn btn-link btn-sm px-0 mx-0 ml-xl-n4" [disabled]="!hasFilters()" (click)="clearSelected()">
|
|
||||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
|
||||||
</svg>
|
|
||||||
Clear all filters
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,239 +0,0 @@
|
|||||||
import { Component, EventEmitter, Input, Output, OnInit, OnDestroy } from '@angular/core';
|
|
||||||
import { PaperlessTag } from 'src/app/data/paperless-tag';
|
|
||||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
|
|
||||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
|
|
||||||
import { Subject, Subscription } from 'rxjs';
|
|
||||||
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
|
||||||
import { NgbDateParserFormatter, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap';
|
|
||||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service';
|
|
||||||
import { TagService } from 'src/app/services/rest/tag.service';
|
|
||||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
|
|
||||||
import { FilterRule } from 'src/app/data/filter-rule';
|
|
||||||
import { FILTER_ADDED_AFTER, FILTER_ADDED_BEFORE, FILTER_CORRESPONDENT, FILTER_CREATED_AFTER, FILTER_CREATED_BEFORE, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_RULE_TYPES, FILTER_TITLE } from 'src/app/data/filter-rule-type';
|
|
||||||
import { DateSelection } from './filter-dropdown-date/filter-dropdown-date.component';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-filter-editor',
|
|
||||||
templateUrl: './filter-editor.component.html',
|
|
||||||
styleUrls: ['./filter-editor.component.scss']
|
|
||||||
})
|
|
||||||
export class FilterEditorComponent implements OnInit, OnDestroy {
|
|
||||||
|
|
||||||
generateFilterName() {
|
|
||||||
if (this.filterRules.length == 1) {
|
|
||||||
let rule = this.filterRules[0]
|
|
||||||
switch(this.filterRules[0].rule_type) {
|
|
||||||
|
|
||||||
case FILTER_CORRESPONDENT:
|
|
||||||
return `Correspondent: ${this.correspondents.find(c => c.id == +rule.value)?.name}`
|
|
||||||
|
|
||||||
case FILTER_DOCUMENT_TYPE:
|
|
||||||
return `Type: ${this.documentTypes.find(dt => dt.id == +rule.value)?.name}`
|
|
||||||
|
|
||||||
case FILTER_HAS_TAG:
|
|
||||||
return `Tag: ${this.tags.find(t => t.id == +rule.value)?.name}`
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private documentTypeService: DocumentTypeService,
|
|
||||||
private tagService: TagService,
|
|
||||||
private correspondentService: CorrespondentService,
|
|
||||||
private dateParser: NgbDateParserFormatter
|
|
||||||
) { }
|
|
||||||
|
|
||||||
tags: PaperlessTag[] = []
|
|
||||||
correspondents: PaperlessCorrespondent[]
|
|
||||||
documentTypes: PaperlessDocumentType[] = []
|
|
||||||
|
|
||||||
@Input()
|
|
||||||
filterRules: FilterRule[]
|
|
||||||
|
|
||||||
@Output()
|
|
||||||
filterRulesChange = new EventEmitter<FilterRule[]>()
|
|
||||||
|
|
||||||
hasFilters() {
|
|
||||||
return this.filterRules.length > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
get selectedTags(): PaperlessTag[] {
|
|
||||||
let tagRules: FilterRule[] = this.filterRules.filter(fr => fr.rule_type == FILTER_HAS_TAG)
|
|
||||||
return this.tags?.filter(t => tagRules.find(tr => +tr.value == t.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
get selectedCorrespondents(): PaperlessCorrespondent[] {
|
|
||||||
let correspondentRules: FilterRule[] = this.filterRules.filter(fr => fr.rule_type == FILTER_CORRESPONDENT)
|
|
||||||
return this.correspondents?.filter(c => correspondentRules.find(cr => +cr.value == c.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
get selectedDocumentTypes(): PaperlessDocumentType[] {
|
|
||||||
let documentTypeRules: FilterRule[] = this.filterRules.filter(fr => fr.rule_type == FILTER_DOCUMENT_TYPE)
|
|
||||||
return this.documentTypes?.filter(dt => documentTypeRules.find(dtr => +dtr.value == dt.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
get titleFilter() {
|
|
||||||
let existingRule = this.filterRules.find(rule => rule.rule_type == FILTER_TITLE)
|
|
||||||
return existingRule ? existingRule.value : ''
|
|
||||||
}
|
|
||||||
|
|
||||||
set titleFilter(value) {
|
|
||||||
this.titleFilterDebounce.next(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
titleFilterDebounce: Subject<string>
|
|
||||||
subscription: Subscription
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.tagService.listAll().subscribe(result => this.tags = result.results)
|
|
||||||
this.correspondentService.listAll().subscribe(result => this.correspondents = result.results)
|
|
||||||
this.documentTypeService.listAll().subscribe(result => this.documentTypes = result.results)
|
|
||||||
|
|
||||||
this.titleFilterDebounce = new Subject<string>()
|
|
||||||
|
|
||||||
this.subscription = this.titleFilterDebounce.pipe(
|
|
||||||
debounceTime(400),
|
|
||||||
distinctUntilChanged()
|
|
||||||
).subscribe(title => {
|
|
||||||
this.setTitleRule(title)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
this.titleFilterDebounce.complete()
|
|
||||||
// TODO: not sure if both is necessary
|
|
||||||
this.subscription.unsubscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
applyFilters() {
|
|
||||||
this.filterRulesChange.next(this.filterRules)
|
|
||||||
}
|
|
||||||
|
|
||||||
clearSelected() {
|
|
||||||
this.filterRules = []
|
|
||||||
this.applyFilters()
|
|
||||||
}
|
|
||||||
|
|
||||||
private toggleFilterRule(filterRuleTypeID: number, value: number) {
|
|
||||||
|
|
||||||
let filterRuleType = FILTER_RULE_TYPES.find(t => t.id == filterRuleTypeID)
|
|
||||||
|
|
||||||
let existingRule = this.filterRules.find(rule => rule.rule_type == filterRuleTypeID && rule.value == value?.toString())
|
|
||||||
let existingRuleOfSameType = this.filterRules.find(rule => rule.rule_type == filterRuleTypeID)
|
|
||||||
|
|
||||||
if (existingRule) {
|
|
||||||
// if this exact rule already exists, remove it in all cases.
|
|
||||||
this.filterRules.splice(this.filterRules.indexOf(existingRule), 1)
|
|
||||||
} else if (filterRuleType.multi || !existingRuleOfSameType) {
|
|
||||||
// if we allow multiple rules per type, or no rule of this type already exists, push a new rule.
|
|
||||||
this.filterRules.push({rule_type: filterRuleTypeID, value: value?.toString()})
|
|
||||||
} else {
|
|
||||||
// otherwise (i.e., no multi support AND there's already a rule of this type), update the rule.
|
|
||||||
existingRuleOfSameType.value = value?.toString()
|
|
||||||
}
|
|
||||||
this.applyFilters()
|
|
||||||
}
|
|
||||||
|
|
||||||
private setTitleRule(title: string) {
|
|
||||||
let existingRule = this.filterRules.find(rule => rule.rule_type == FILTER_TITLE)
|
|
||||||
|
|
||||||
if (!existingRule && title) {
|
|
||||||
this.filterRules.push({rule_type: FILTER_TITLE, value: title})
|
|
||||||
} else if (existingRule && !title) {
|
|
||||||
this.filterRules.splice(this.filterRules.findIndex(rule => rule.rule_type == FILTER_TITLE), 1)
|
|
||||||
} else if (existingRule && title) {
|
|
||||||
existingRule.value = title
|
|
||||||
}
|
|
||||||
this.applyFilters()
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleTag(tagId: number) {
|
|
||||||
this.toggleFilterRule(FILTER_HAS_TAG, tagId)
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleCorrespondent(correspondentId: number) {
|
|
||||||
this.toggleFilterRule(FILTER_CORRESPONDENT, correspondentId)
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleDocumentType(documentTypeId: number) {
|
|
||||||
this.toggleFilterRule(FILTER_DOCUMENT_TYPE, documentTypeId)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Date handling
|
|
||||||
|
|
||||||
|
|
||||||
onDatesCreatedSet(dates: DateSelection) {
|
|
||||||
this.setDateCreatedBefore(dates.before)
|
|
||||||
this.setDateCreatedAfter(dates.after)
|
|
||||||
this.applyFilters()
|
|
||||||
}
|
|
||||||
|
|
||||||
onDatesAddedSet(dates: DateSelection) {
|
|
||||||
this.setDateAddedBefore(dates.before)
|
|
||||||
this.setDateAddedAfter(dates.after)
|
|
||||||
this.applyFilters()
|
|
||||||
}
|
|
||||||
|
|
||||||
get dateCreatedBefore(): string {
|
|
||||||
let createdBeforeRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_CREATED_BEFORE)
|
|
||||||
return createdBeforeRule ? createdBeforeRule.value : null
|
|
||||||
}
|
|
||||||
|
|
||||||
get dateCreatedAfter(): string {
|
|
||||||
let createdAfterRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_CREATED_AFTER)
|
|
||||||
return createdAfterRule ? createdAfterRule.value : null
|
|
||||||
}
|
|
||||||
|
|
||||||
get dateAddedBefore(): string {
|
|
||||||
let addedBeforeRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_ADDED_BEFORE)
|
|
||||||
return addedBeforeRule ? addedBeforeRule.value : null
|
|
||||||
}
|
|
||||||
|
|
||||||
get dateAddedAfter(): string {
|
|
||||||
let addedAfterRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_ADDED_AFTER)
|
|
||||||
return addedAfterRule ? addedAfterRule.value : null
|
|
||||||
}
|
|
||||||
|
|
||||||
setDateCreatedBefore(date?: string) {
|
|
||||||
if (date) this.setDateFilter(date, FILTER_CREATED_BEFORE)
|
|
||||||
else this.clearDateFilter(FILTER_CREATED_BEFORE)
|
|
||||||
}
|
|
||||||
|
|
||||||
setDateCreatedAfter(date?: string) {
|
|
||||||
if (date) this.setDateFilter(date, FILTER_CREATED_AFTER)
|
|
||||||
else this.clearDateFilter(FILTER_CREATED_AFTER)
|
|
||||||
}
|
|
||||||
|
|
||||||
setDateAddedBefore(date?: string) {
|
|
||||||
if (date) this.setDateFilter(date, FILTER_ADDED_BEFORE)
|
|
||||||
else this.clearDateFilter(FILTER_ADDED_BEFORE)
|
|
||||||
}
|
|
||||||
|
|
||||||
setDateAddedAfter(date?: string) {
|
|
||||||
if (date) this.setDateFilter(date, FILTER_ADDED_AFTER)
|
|
||||||
else this.clearDateFilter(FILTER_ADDED_AFTER)
|
|
||||||
}
|
|
||||||
|
|
||||||
setDateFilter(date: string, dateRuleTypeID: number) {
|
|
||||||
let existingRule = this.filterRules.find(rule => rule.rule_type == dateRuleTypeID)
|
|
||||||
|
|
||||||
if (existingRule) {
|
|
||||||
existingRule.value = date
|
|
||||||
} else {
|
|
||||||
this.filterRules.push({rule_type: dateRuleTypeID, value: date})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clearDateFilter(dateRuleTypeID: number) {
|
|
||||||
let ruleIndex = this.filterRules.findIndex(rule => rule.rule_type == dateRuleTypeID)
|
|
||||||
if (ruleIndex != -1) {
|
|
||||||
this.filterRules.splice(ruleIndex, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -7,13 +7,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|
||||||
<app-input-text title="Name" formControlName="name"></app-input-text>
|
<app-input-text i18n-title title="Name" formControlName="name"></app-input-text>
|
||||||
<app-input-select title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
||||||
<app-input-text title="Match" formControlName="match" hint="Auto matching does not require you to fill in this field."></app-input-text>
|
<app-input-text i18n-title title="Match" formControlName="match" i18n-hint hint="Auto matching does not require you to fill in this field."></app-input-text>
|
||||||
<app-input-check title="Case insensitive" formControlName="is_insensitive" hint="Auto matching ignores this option."></app-input-check>
|
<app-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" i18n-hint hint="Auto matching ignores this option."></app-input-check>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button>
|
<button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n>Cancel</button>
|
||||||
<button type="submit" class="btn btn-primary">Save</button>
|
<button type="submit" class="btn btn-primary" i18n>Save</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
@ -14,7 +14,19 @@ import { ToastService } from 'src/app/services/toast.service';
|
|||||||
export class CorrespondentEditDialogComponent extends EditDialogComponent<PaperlessCorrespondent> {
|
export class CorrespondentEditDialogComponent extends EditDialogComponent<PaperlessCorrespondent> {
|
||||||
|
|
||||||
constructor(service: CorrespondentService, activeModal: NgbActiveModal, toastService: ToastService) {
|
constructor(service: CorrespondentService, activeModal: NgbActiveModal, toastService: ToastService) {
|
||||||
super(service, activeModal, toastService, 'correspondent')
|
super(service, activeModal, toastService)
|
||||||
|
}
|
||||||
|
|
||||||
|
getCreateTitle() {
|
||||||
|
return $localize`Create new correspondent`
|
||||||
|
}
|
||||||
|
|
||||||
|
getEditTitle() {
|
||||||
|
return $localize`Edit correspondent`
|
||||||
|
}
|
||||||
|
|
||||||
|
getSaveErrorMessage(error: string) {
|
||||||
|
return $localize`Could not save correspondent: ${error}`
|
||||||
}
|
}
|
||||||
|
|
||||||
getForm(): FormGroup {
|
getForm(): FormGroup {
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
<app-page-header title="Correspondents">
|
<app-page-header title="Correspondents" i18n-title>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()">
|
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" i18n>Create</button>
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
</app-page-header>
|
</app-page-header>
|
||||||
|
|
||||||
<div class="row m-0 justify-content-end">
|
<div class="row m-0 justify-content-end">
|
||||||
@ -11,11 +9,11 @@
|
|||||||
<table class="table table-striped border shadow">
|
<table class="table table-striped border shadow">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" sortable="name" (sort)="onSort($event)">Name</th>
|
<th scope="col" sortable="name" (sort)="onSort($event)" i18n>Name</th>
|
||||||
<th scope="col" sortable="matching_algorithm" (sort)="onSort($event)">Matching</th>
|
<th scope="col" sortable="matching_algorithm" (sort)="onSort($event)" i18n>Matching</th>
|
||||||
<th scope="col" sortable="document_count" (sort)="onSort($event)">Document count</th>
|
<th scope="col" sortable="document_count" (sort)="onSort($event)" i18n>Document count</th>
|
||||||
<th scope="col" sortable="last_correspondence" (sort)="onSort($event)">Last correspondence</th>
|
<th scope="col" sortable="last_correspondence" (sort)="onSort($event)" i18n>Last correspondence</th>
|
||||||
<th scope="col">Actions</th>
|
<th scope="col" i18n>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -29,21 +27,18 @@
|
|||||||
<button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(correspondent)">
|
<button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(correspondent)">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-funnel" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-funnel" viewBox="0 0 16 16">
|
||||||
<path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2h-11z"/>
|
<path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2h-11z"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>Documents</ng-container>
|
||||||
Documents
|
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(correspondent)">
|
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(correspondent)">
|
||||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>Edit</ng-container>
|
||||||
Edit
|
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(correspondent)">
|
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(correspondent)">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
||||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
||||||
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>Delete</ng-container>
|
||||||
Delete
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { FILTER_CORRESPONDENT } from 'src/app/data/filter-rule-type';
|
import { FILTER_CORRESPONDENT } from 'src/app/data/filter-rule-type';
|
||||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
|
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
|
||||||
@ -16,20 +15,16 @@ import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog/co
|
|||||||
export class CorrespondentListComponent extends GenericListComponent<PaperlessCorrespondent> {
|
export class CorrespondentListComponent extends GenericListComponent<PaperlessCorrespondent> {
|
||||||
|
|
||||||
constructor(correspondentsService: CorrespondentService, modalService: NgbModal,
|
constructor(correspondentsService: CorrespondentService, modalService: NgbModal,
|
||||||
private router: Router,
|
|
||||||
private list: DocumentListViewService
|
private list: DocumentListViewService
|
||||||
) {
|
) {
|
||||||
super(correspondentsService,modalService,CorrespondentEditDialogComponent)
|
super(correspondentsService,modalService,CorrespondentEditDialogComponent)
|
||||||
}
|
}
|
||||||
|
|
||||||
getObjectName(object: PaperlessCorrespondent) {
|
getDeleteMessage(object: PaperlessCorrespondent) {
|
||||||
return `correspondent '${object.name}'`
|
return $localize`Do you really want to delete the correspondent "${object.name}"?`
|
||||||
}
|
}
|
||||||
|
|
||||||
filterDocuments(object: PaperlessCorrespondent) {
|
filterDocuments(object: PaperlessCorrespondent) {
|
||||||
this.list.documentListView.filter_rules = [
|
this.list.quickFilter([{rule_type: FILTER_CORRESPONDENT, value: object.id.toString()}])
|
||||||
{rule_type: FILTER_CORRESPONDENT, value: object.id.toString()}
|
|
||||||
]
|
|
||||||
this.router.navigate(["documents"])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,14 +7,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|
||||||
<app-input-text title="Name" formControlName="name"></app-input-text>
|
<app-input-text i18n-title title="Name" formControlName="name"></app-input-text>
|
||||||
<app-input-select title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
||||||
<app-input-text title="Match" formControlName="match" hint="Auto matching does not require you to fill in this field."></app-input-text>
|
<app-input-text i18n-title title="Match" formControlName="match" i18n-hint hint="Auto matching does not require you to fill in this field."></app-input-text>
|
||||||
<app-input-check title="Case insensitive" formControlName="is_insensitive" hint="Auto matching ignores this option."></app-input-check>
|
<app-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" i18n-hint hint="Auto matching ignores this option."></app-input-check>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button>
|
<button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n>Cancel</button>
|
||||||
<button type="submit" class="btn btn-primary">Save</button>
|
<button type="submit" class="btn btn-primary" i18n>Save</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
@ -14,7 +14,19 @@ import { ToastService } from 'src/app/services/toast.service';
|
|||||||
export class DocumentTypeEditDialogComponent extends EditDialogComponent<PaperlessDocumentType> {
|
export class DocumentTypeEditDialogComponent extends EditDialogComponent<PaperlessDocumentType> {
|
||||||
|
|
||||||
constructor(service: DocumentTypeService, activeModal: NgbActiveModal, toastService: ToastService) {
|
constructor(service: DocumentTypeService, activeModal: NgbActiveModal, toastService: ToastService) {
|
||||||
super(service, activeModal, toastService, 'document type')
|
super(service, activeModal, toastService)
|
||||||
|
}
|
||||||
|
|
||||||
|
getCreateTitle() {
|
||||||
|
return $localize`Create new document type`
|
||||||
|
}
|
||||||
|
|
||||||
|
getEditTitle() {
|
||||||
|
return $localize`Edit document type`
|
||||||
|
}
|
||||||
|
|
||||||
|
getSaveErrorMessage(error: string) {
|
||||||
|
return $localize`Could not save document type: ${error}`
|
||||||
}
|
}
|
||||||
|
|
||||||
getForm(): FormGroup {
|
getForm(): FormGroup {
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
<app-page-header title="Document types">
|
<app-page-header title="Document types" i18n-title>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()">
|
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" i18n>Create</button>
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
</app-page-header>
|
</app-page-header>
|
||||||
|
|
||||||
<div class="row m-0 justify-content-end">
|
<div class="row m-0 justify-content-end">
|
||||||
@ -12,10 +10,10 @@
|
|||||||
<table class="table table-striped border shadow">
|
<table class="table table-striped border shadow">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" sortable="name" (sort)="onSort($event)">Name</th>
|
<th scope="col" sortable="name" (sort)="onSort($event)" i18n>Name</th>
|
||||||
<th scope="col" sortable="matching_algorithm" (sort)="onSort($event)">Matching</th>
|
<th scope="col" sortable="matching_algorithm" (sort)="onSort($event)" i18n>Matching</th>
|
||||||
<th scope="col" sortable="document_count" (sort)="onSort($event)">Document count</th>
|
<th scope="col" sortable="document_count" (sort)="onSort($event)" i18n>Document count</th>
|
||||||
<th scope="col">Actions</th>
|
<th scope="col" i18n>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -28,21 +26,18 @@
|
|||||||
<button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(document_type)">
|
<button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(document_type)">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-funnel" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-funnel" viewBox="0 0 16 16">
|
||||||
<path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2h-11z"/>
|
<path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2h-11z"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>Documents</ng-container>
|
||||||
Documents
|
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(document_type)">
|
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(document_type)">
|
||||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>Edit</ng-container>
|
||||||
Edit
|
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(document_type)">
|
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(document_type)">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
||||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
||||||
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>Delete</ng-container>
|
||||||
Delete
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { FILTER_DOCUMENT_TYPE } from 'src/app/data/filter-rule-type';
|
import { FILTER_DOCUMENT_TYPE } from 'src/app/data/filter-rule-type';
|
||||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
|
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
|
||||||
@ -16,20 +15,17 @@ import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog/doc
|
|||||||
export class DocumentTypeListComponent extends GenericListComponent<PaperlessDocumentType> {
|
export class DocumentTypeListComponent extends GenericListComponent<PaperlessDocumentType> {
|
||||||
|
|
||||||
constructor(service: DocumentTypeService, modalService: NgbModal,
|
constructor(service: DocumentTypeService, modalService: NgbModal,
|
||||||
private router: Router,
|
|
||||||
private list: DocumentListViewService
|
private list: DocumentListViewService
|
||||||
) {
|
) {
|
||||||
super(service, modalService, DocumentTypeEditDialogComponent)
|
super(service, modalService, DocumentTypeEditDialogComponent)
|
||||||
}
|
}
|
||||||
|
|
||||||
getObjectName(object: PaperlessDocumentType) {
|
getDeleteMessage(object: PaperlessDocumentType) {
|
||||||
return `document type '${object.name}'`
|
return $localize`Do you really want to delete the document type "${object.name}"?`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
filterDocuments(object: PaperlessDocumentType) {
|
filterDocuments(object: PaperlessDocumentType) {
|
||||||
this.list.documentListView.filter_rules = [
|
this.list.quickFilter([{rule_type: FILTER_DOCUMENT_TYPE, value: object.id.toString()}])
|
||||||
{rule_type: FILTER_DOCUMENT_TYPE, value: object.id.toString()}
|
|
||||||
]
|
|
||||||
this.router.navigate(["documents"])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On
|
|||||||
|
|
||||||
getMatching(o: MatchingModel) {
|
getMatching(o: MatchingModel) {
|
||||||
if (o.matching_algorithm == MATCH_AUTO) {
|
if (o.matching_algorithm == MATCH_AUTO) {
|
||||||
return "Automatic"
|
return $localize`Automatic`
|
||||||
} else if (o.match && o.match.length > 0) {
|
} else if (o.match && o.match.length > 0) {
|
||||||
return `${o.match} (${MATCHING_ALGORITHMS.find(a => a.id == o.matching_algorithm).name})`
|
return `${o.match} (${MATCHING_ALGORITHMS.find(a => a.id == o.matching_algorithm).name})`
|
||||||
} else {
|
} else {
|
||||||
@ -84,17 +84,17 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getObjectName(object: T) {
|
getDeleteMessage(object: T) {
|
||||||
return object.toString()
|
return $localize`Do you really want to delete this element?`
|
||||||
}
|
}
|
||||||
|
|
||||||
openDeleteDialog(object: T) {
|
openDeleteDialog(object: T) {
|
||||||
var activeModal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
var activeModal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
||||||
activeModal.componentInstance.title = "Confirm delete"
|
activeModal.componentInstance.title = $localize`Confirm delete`
|
||||||
activeModal.componentInstance.messageBold = `Do you really want to delete ${this.getObjectName(object)}?`
|
activeModal.componentInstance.messageBold = this.getDeleteMessage(object)
|
||||||
activeModal.componentInstance.message = "Associated documents will not be deleted."
|
activeModal.componentInstance.message = $localize`Associated documents will not be deleted.`
|
||||||
activeModal.componentInstance.btnClass = "btn-danger"
|
activeModal.componentInstance.btnClass = "btn-danger"
|
||||||
activeModal.componentInstance.btnCaption = "Delete"
|
activeModal.componentInstance.btnCaption = $localize`Delete`
|
||||||
activeModal.componentInstance.confirmClicked.subscribe(() => {
|
activeModal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
this.service.delete(object).subscribe(_ => {
|
this.service.delete(object).subscribe(_ => {
|
||||||
activeModal.close()
|
activeModal.close()
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
<app-page-header title="Logs">
|
<app-page-header title="Logs" i18n-title>
|
||||||
|
|
||||||
<div ngbDropdown class="btn-group">
|
<div ngbDropdown class="btn-group">
|
||||||
<button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle>
|
<button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle>
|
||||||
<svg class="toolbaricon" fill="currentColor">
|
<svg class="toolbaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#funnel" />
|
<use xlink:href="assets/bootstrap-icons.svg#funnel" />
|
||||||
</svg>
|
</svg> <ng-container i18n>Filter</ng-container>
|
||||||
Filter
|
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
|
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
|
||||||
<button *ngFor="let f of getLevels()" ngbDropdownItem (click)="setLevel(f.id)"
|
<button *ngFor="let f of getLevels()" ngbDropdownItem (click)="setLevel(f.id)"
|
||||||
|
@ -7,14 +7,14 @@
|
|||||||
|
|
||||||
<ul ngbNav #nav="ngbNav" class="nav-tabs">
|
<ul ngbNav #nav="ngbNav" class="nav-tabs">
|
||||||
<li [ngbNavItem]="1">
|
<li [ngbNavItem]="1">
|
||||||
<a ngbNavLink>General settings</a>
|
<a ngbNavLink i18n>General settings</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
|
|
||||||
<h4>Document list</h4>
|
<h4 i18n>Document list</h4>
|
||||||
|
|
||||||
<div class="form-row form-group">
|
<div class="form-row form-group">
|
||||||
<div class="col-md-3 col-form-label">
|
<div class="col-md-3 col-form-label">
|
||||||
<span>Items per page</span>
|
<span i18n>Items per page</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
|
||||||
@ -26,41 +26,48 @@
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h4 i18n>Bulk editing</h4>
|
||||||
|
|
||||||
|
<app-input-check i18n-title title="Show confirmation dialogs" formControlName="bulkEditConfirmationDialogs" i18n-hint hint="Deleting documents will always ask for confirmation."></app-input-check>
|
||||||
|
<app-input-check i18n-title title="Apply on close" formControlName="bulkEditApplyOnClose"></app-input-check>
|
||||||
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
<li [ngbNavItem]="2">
|
<li [ngbNavItem]="2">
|
||||||
<a ngbNavLink>Saved views</a>
|
<a ngbNavLink i18n>Saved views</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
|
|
||||||
<div formGroupName="savedViews">
|
<div formGroupName="savedViews">
|
||||||
|
|
||||||
<div *ngFor="let view of savedViews" [formGroupName]="view.id" class="form-row">
|
<div *ngFor="let view of savedViews" [formGroupName]="view.id" class="form-row">
|
||||||
<div class="form-group col-4 mr-3">
|
<div class="form-group col-4 mr-3">
|
||||||
<label for="name_{{view.id}}">Name</label>
|
<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="form-group col-auto mr-3">
|
<div class="form-group col-auto mr-3">
|
||||||
<label for="show_on_dashboard_{{view.id}}">Appears on</label>
|
<label for="show_on_dashboard_{{view.id}}" i18n>Appears on</label>
|
||||||
<div class="custom-control custom-switch">
|
<div class="custom-control custom-switch">
|
||||||
<input type="checkbox" class="custom-control-input" id="show_on_dashboard_{{view.id}}" formControlName="show_on_dashboard">
|
<input type="checkbox" class="custom-control-input" id="show_on_dashboard_{{view.id}}" formControlName="show_on_dashboard">
|
||||||
<label class="custom-control-label" for="show_on_dashboard_{{view.id}}">Show on dashboard</label>
|
<label class="custom-control-label" for="show_on_dashboard_{{view.id}}" i18n>Show on dashboard</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-control custom-switch">
|
<div class="custom-control custom-switch">
|
||||||
<input type="checkbox" class="custom-control-input" id="show_in_sidebar_{{view.id}}" formControlName="show_in_sidebar">
|
<input type="checkbox" class="custom-control-input" id="show_in_sidebar_{{view.id}}" formControlName="show_in_sidebar">
|
||||||
<label class="custom-control-label" for="show_in_sidebar_{{view.id}}">Show in sidebar</label>
|
<label class="custom-control-label" for="show_in_sidebar_{{view.id}}" i18n>Show in sidebar</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group col-auto">
|
<div class="form-group col-auto">
|
||||||
<label for="name_{{view.id}}">Actions</label>
|
<label for="name_{{view.id}}" i18n>Actions</label>
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger form-control" (click)="deleteSavedView(view)">Delete</button>
|
<button type="button" class="btn btn-sm btn-outline-danger form-control" (click)="deleteSavedView(view)" i18n>Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="savedViews.length == 0">No saved views defined.</div>
|
<div *ngIf="savedViews.length == 0" i18n>No saved views defined.</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { FormControl, FormGroup } from '@angular/forms';
|
import { FormControl, FormGroup } from '@angular/forms';
|
||||||
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
|
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
|
||||||
import { GENERAL_SETTINGS } from 'src/app/data/storage-keys';
|
|
||||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
||||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
|
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
|
||||||
import { Toast, ToastService } from 'src/app/services/toast.service';
|
import { SettingsService, SETTINGS_KEYS } from 'src/app/services/settings.service';
|
||||||
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-settings',
|
selector: 'app-settings',
|
||||||
@ -16,14 +16,17 @@ export class SettingsComponent implements OnInit {
|
|||||||
savedViewGroup = new FormGroup({})
|
savedViewGroup = new FormGroup({})
|
||||||
|
|
||||||
settingsForm = new FormGroup({
|
settingsForm = new FormGroup({
|
||||||
'documentListItemPerPage': new FormControl(+localStorage.getItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE) || GENERAL_SETTINGS.DOCUMENT_LIST_SIZE_DEFAULT),
|
'bulkEditConfirmationDialogs': new FormControl(this.settings.get(SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS)),
|
||||||
|
'bulkEditApplyOnClose': new FormControl(this.settings.get(SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE)),
|
||||||
|
'documentListItemPerPage': new FormControl(this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)),
|
||||||
'savedViews': this.savedViewGroup
|
'savedViews': this.savedViewGroup
|
||||||
})
|
})
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public savedViewService: SavedViewService,
|
public savedViewService: SavedViewService,
|
||||||
private documentListViewService: DocumentListViewService,
|
private documentListViewService: DocumentListViewService,
|
||||||
private toastService: ToastService
|
private toastService: ToastService,
|
||||||
|
private settings: SettingsService
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
savedViews: PaperlessSavedView[]
|
savedViews: PaperlessSavedView[]
|
||||||
@ -46,14 +49,16 @@ export class SettingsComponent implements OnInit {
|
|||||||
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)
|
||||||
this.toastService.showToast(Toast.make("Information", `Saved view "${savedView.name} deleted.`))
|
this.toastService.showInfo($localize`Saved view "${savedView.name} deleted.`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private saveLocalSettings() {
|
private saveLocalSettings() {
|
||||||
localStorage.setItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE, this.settingsForm.value.documentListItemPerPage)
|
this.settings.set(SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE, this.settingsForm.value.bulkEditApplyOnClose)
|
||||||
|
this.settings.set(SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS, this.settingsForm.value.bulkEditConfirmationDialogs)
|
||||||
|
this.settings.set(SETTINGS_KEYS.DOCUMENT_LIST_SIZE, this.settingsForm.value.documentListItemPerPage)
|
||||||
this.documentListViewService.updatePageSize()
|
this.documentListViewService.updatePageSize()
|
||||||
this.toastService.showToast(Toast.make("Information", "Settings saved successfully."))
|
this.toastService.showInfo($localize`Settings saved successfully.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
saveSettings() {
|
saveSettings() {
|
||||||
@ -65,7 +70,7 @@ export class SettingsComponent implements OnInit {
|
|||||||
this.savedViewService.patchMany(x).subscribe(s => {
|
this.savedViewService.patchMany(x).subscribe(s => {
|
||||||
this.saveLocalSettings()
|
this.saveLocalSettings()
|
||||||
}, error => {
|
}, error => {
|
||||||
this.toastService.showToast(Toast.makeError(`Error while storing settings on server: ${JSON.stringify(error.error)}`))
|
this.toastService.showError($localize`Error while storing settings on server: ${JSON.stringify(error.error)}`)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.saveLocalSettings()
|
this.saveLocalSettings()
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<div class="form-group paperless-input-select">
|
<div class="form-group paperless-input-select">
|
||||||
<label for="colour">Colour</label>
|
<label for="colour" i18n>Color</label>
|
||||||
<ng-select name="colour" formControlName="colour" [items]="getColours()" bindValue="id" bindLabel="name" [clearable]="false">
|
<ng-select name="colour" formControlName="colour" [items]="getColours()" bindValue="id" bindLabel="name" [clearable]="false">
|
||||||
<ng-template ng-option-tmp ng-label-tmp let-item="item">
|
<ng-template ng-option-tmp ng-label-tmp let-item="item">
|
||||||
<span class="badge" [style.background]="item.value" [style.color]="item.textColor">{{item.name}}</span>
|
<span class="badge" [style.background]="item.value" [style.color]="item.textColor">{{item.name}}</span>
|
||||||
@ -18,13 +18,13 @@
|
|||||||
</ng-select>
|
</ng-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<app-input-check title="Inbox tag" formControlName="is_inbox_tag" hint="Inbox tags are automatically assigned to all consumed documents."></app-input-check>
|
<app-input-check i18n-title title="Inbox tag" formControlName="is_inbox_tag" i18n-hint hint="Inbox tags are automatically assigned to all consumed documents."></app-input-check>
|
||||||
<app-input-select title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
||||||
<app-input-text title="Match" formControlName="match" hint="Auto matching does not require you to fill in this field."></app-input-text>
|
<app-input-text i18n-title title="Match" formControlName="match" i18n-hint hint="Auto matching does not require you to fill in this field."></app-input-text>
|
||||||
<app-input-check title="Case insensitive" formControlName="is_insensitive" hint="Auto matching ignores this option."></app-input-check>
|
<app-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" i18n-hint hint="Auto matching ignores this option."></app-input-check>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button>
|
<button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n>Cancel</button>
|
||||||
<button type="submit" class="btn btn-primary">Save</button>
|
<button type="submit" class="btn btn-primary" i18n>Save</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -14,7 +14,19 @@ import { ToastService } from 'src/app/services/toast.service';
|
|||||||
export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> {
|
export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> {
|
||||||
|
|
||||||
constructor(service: TagService, activeModal: NgbActiveModal, toastService: ToastService) {
|
constructor(service: TagService, activeModal: NgbActiveModal, toastService: ToastService) {
|
||||||
super(service, activeModal, toastService, 'tag')
|
super(service, activeModal, toastService)
|
||||||
|
}
|
||||||
|
|
||||||
|
getCreateTitle() {
|
||||||
|
return $localize`Create new tag`
|
||||||
|
}
|
||||||
|
|
||||||
|
getEditTitle() {
|
||||||
|
return $localize`Edit tag`
|
||||||
|
}
|
||||||
|
|
||||||
|
getSaveErrorMessage(error: string) {
|
||||||
|
return $localize`Could not save tag: ${error}`
|
||||||
}
|
}
|
||||||
|
|
||||||
getForm(): FormGroup {
|
getForm(): FormGroup {
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
<app-page-header title="Tags">
|
<app-page-header title="Tags" i18n-title>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()">
|
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" i18n>Create</button>
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
</app-page-header>
|
</app-page-header>
|
||||||
|
|
||||||
<div class="row m-0 justify-content-end">
|
<div class="row m-0 justify-content-end">
|
||||||
@ -12,11 +10,11 @@
|
|||||||
<table class="table table-striped border shadow-sm">
|
<table class="table table-striped border shadow-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" sortable="name" (sort)="onSort($event)">Name</th>
|
<th scope="col" sortable="name" (sort)="onSort($event)" i18n>Name</th>
|
||||||
<th scope="col">Colour</th>
|
<th scope="col" i18n>Color</th>
|
||||||
<th scope="col" sortable="matching_algorithm" (sort)="onSort($event)">Matching</th>
|
<th scope="col" sortable="matching_algorithm" (sort)="onSort($event)" i18n>Matching</th>
|
||||||
<th scope="col" sortable="document_count" (sort)="onSort($event)">Document count</th>
|
<th scope="col" sortable="document_count" (sort)="onSort($event)" i18n>Document count</th>
|
||||||
<th scope="col">Actions</th>
|
<th scope="col" i18n>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -31,21 +29,18 @@
|
|||||||
<button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(tag)">
|
<button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(tag)">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-funnel" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-funnel" viewBox="0 0 16 16">
|
||||||
<path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2h-11z"/>
|
<path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2h-11z"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>Documents</ng-container>
|
||||||
Documents
|
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(tag)">
|
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(tag)">
|
||||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>Edit</ng-container>
|
||||||
Edit
|
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(tag)">
|
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(tag)">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
||||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
||||||
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
||||||
</svg>
|
</svg> <ng-container i18n>Delete</ng-container>
|
||||||
Delete
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { FILTER_HAS_TAG } from 'src/app/data/filter-rule-type';
|
import { FILTER_HAS_TAG } from 'src/app/data/filter-rule-type';
|
||||||
import { TAG_COLOURS, PaperlessTag } from 'src/app/data/paperless-tag';
|
import { TAG_COLOURS, PaperlessTag } from 'src/app/data/paperless-tag';
|
||||||
@ -16,7 +15,6 @@ import { TagEditDialogComponent } from './tag-edit-dialog/tag-edit-dialog.compon
|
|||||||
export class TagListComponent extends GenericListComponent<PaperlessTag> {
|
export class TagListComponent extends GenericListComponent<PaperlessTag> {
|
||||||
|
|
||||||
constructor(tagService: TagService, modalService: NgbModal,
|
constructor(tagService: TagService, modalService: NgbModal,
|
||||||
private router: Router,
|
|
||||||
private list: DocumentListViewService
|
private list: DocumentListViewService
|
||||||
) {
|
) {
|
||||||
super(tagService, modalService, TagEditDialogComponent)
|
super(tagService, modalService, TagEditDialogComponent)
|
||||||
@ -26,14 +24,12 @@ export class TagListComponent extends GenericListComponent<PaperlessTag> {
|
|||||||
return TAG_COLOURS.find(c => c.id == id)
|
return TAG_COLOURS.find(c => c.id == id)
|
||||||
}
|
}
|
||||||
|
|
||||||
getObjectName(object: PaperlessTag) {
|
getDeleteMessage(object: PaperlessTag) {
|
||||||
return `tag '${object.name}'`
|
return $localize`Do you really want to delete the tag "${object.name}"?`
|
||||||
}
|
}
|
||||||
|
|
||||||
filterDocuments(object: PaperlessTag) {
|
filterDocuments(object: PaperlessTag) {
|
||||||
this.list.documentListView.filter_rules = [
|
this.list.quickFilter([{rule_type: FILTER_HAS_TAG, value: object.id.toString()}])
|
||||||
{rule_type: FILTER_HAS_TAG, value: object.id.toString()}
|
|
||||||
]
|
|
||||||
this.router.navigate(["documents"])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,5 +4,5 @@
|
|||||||
<path fill-rule="evenodd" d="M4.285 12.433a.5.5 0 0 0 .683-.183A3.498 3.498 0 0 1 8 10.5c1.295 0 2.426.703 3.032 1.75a.5.5 0 0 0 .866-.5A4.498 4.498 0 0 0 8 9.5a4.5 4.5 0 0 0-3.898 2.25.5.5 0 0 0 .183.683z"/>
|
<path fill-rule="evenodd" d="M4.285 12.433a.5.5 0 0 0 .683-.183A3.498 3.498 0 0 1 8 10.5c1.295 0 2.426.703 3.032 1.75a.5.5 0 0 0 .866-.5A4.498 4.498 0 0 0 8 9.5a4.5 4.5 0 0 0-3.898 2.25.5.5 0 0 0 .183.683z"/>
|
||||||
<path d="M7 6.5C7 7.328 6.552 8 6 8s-1-.672-1-1.5S5.448 5 6 5s1 .672 1 1.5zm4 0c0 .828-.448 1.5-1 1.5s-1-.672-1-1.5S9.448 5 10 5s1 .672 1 1.5z"/>
|
<path d="M7 6.5C7 7.328 6.552 8 6 8s-1-.672-1-1.5S5.448 5 6 5s1 .672 1 1.5zm4 0c0 .828-.448 1.5-1 1.5s-1-.672-1-1.5S9.448 5 10 5s1 .672 1 1.5z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<h1>404 Not Found</h1>
|
<h1 i18n>404 Not Found</h1>
|
||||||
</div>
|
</div>
|
@ -1,23 +1,21 @@
|
|||||||
<app-page-header title="Search results">
|
<app-page-header i18n-title title="Search results">
|
||||||
</app-page-header>
|
</app-page-header>
|
||||||
|
|
||||||
<div *ngIf="errorMessage" class="alert alert-danger">Invalid search query: {{errorMessage}}</div>
|
<div *ngIf="errorMessage" class="alert alert-danger" i18n>Invalid search query: {{errorMessage}}</div>
|
||||||
|
|
||||||
<p *ngIf="more_like">
|
<p *ngIf="more_like" i18n>
|
||||||
Showing documents similar to
|
Showing documents similar to <a routerLink="/documents/{{more_like}}">{{more_like_doc?.original_file_name}}</a>
|
||||||
<a routerLink="/documents/{{more_like}}">{{more_like_doc?.original_file_name}}</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p *ngIf="query">
|
<p *ngIf="query">
|
||||||
Search string: <i>{{query}}</i>
|
<ng-container i18n>Search query: <i>{{query}}</i></ng-container>
|
||||||
<ng-container *ngIf="correctedQuery">
|
<ng-container *ngIf="correctedQuery">
|
||||||
- Did you mean "<a [routerLink]="" (click)="searchCorrectedQuery()">{{correctedQuery}}</a>"?
|
- <ng-container i18n>Did you mean "<a [routerLink]="" (click)="searchCorrectedQuery()">{{correctedQuery}}</a>"?</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div *ngIf="!errorMessage" [class.result-content-searching]="searching" infiniteScroll (scrolled)="onScroll()">
|
<div *ngIf="!errorMessage" [class.result-content-searching]="searching" infiniteScroll (scrolled)="onScroll()">
|
||||||
<p>{{resultCount}} result(s)</p>
|
<p i18n>{resultCount, plural, =0 {No results} =1 {One result} other {{{resultCount}} results}}</p>
|
||||||
<app-document-card-large *ngFor="let result of results"
|
<app-document-card-large *ngFor="let result of results"
|
||||||
[document]="result.document"
|
[document]="result.document"
|
||||||
[details]="result.highlights"
|
[details]="result.highlights"
|
||||||
|
@ -25,8 +25,8 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [
|
|||||||
|
|
||||||
{id: FILTER_ASN, name: "ASN is", filtervar: "archive_serial_number", datatype: "number", multi: false},
|
{id: FILTER_ASN, name: "ASN is", filtervar: "archive_serial_number", datatype: "number", multi: false},
|
||||||
|
|
||||||
{id: FILTER_CORRESPONDENT, name: "Correspondent is", filtervar: "correspondent__id", datatype: "correspondent", multi: false},
|
{id: FILTER_CORRESPONDENT, name: "Correspondent is", filtervar: "correspondent__id", isnull_filtervar: "correspondent__isnull", datatype: "correspondent", multi: false},
|
||||||
{id: FILTER_DOCUMENT_TYPE, name: "Document type is", filtervar: "document_type__id", datatype: "document_type", multi: false},
|
{id: FILTER_DOCUMENT_TYPE, name: "Document type is", filtervar: "document_type__id", isnull_filtervar: "document_type__isnull", datatype: "document_type", multi: false},
|
||||||
|
|
||||||
{id: FILTER_IS_IN_INBOX, name: "Is in Inbox", filtervar: "is_in_inbox", datatype: "boolean", multi: false, default: true},
|
{id: FILTER_IS_IN_INBOX, name: "Is in Inbox", filtervar: "is_in_inbox", datatype: "boolean", multi: false, default: true},
|
||||||
{id: FILTER_HAS_TAG, name: "Has tag", filtervar: "tags__id__all", datatype: "tag", multi: true},
|
{id: FILTER_HAS_TAG, name: "Has tag", filtervar: "tags__id__all", datatype: "tag", multi: true},
|
||||||
@ -51,6 +51,7 @@ export interface FilterRuleType {
|
|||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
filtervar: string
|
filtervar: string
|
||||||
|
isnull_filtervar?: string
|
||||||
datatype: string //number, string, boolean, date
|
datatype: string //number, string, boolean, date
|
||||||
multi: boolean
|
multi: boolean
|
||||||
default?: any
|
default?: any
|
||||||
|
@ -9,12 +9,12 @@ export const MATCH_FUZZY = 5
|
|||||||
export const MATCH_AUTO = 6
|
export const MATCH_AUTO = 6
|
||||||
|
|
||||||
export const MATCHING_ALGORITHMS = [
|
export const MATCHING_ALGORITHMS = [
|
||||||
{id: MATCH_ANY, name: "Any"},
|
{id: MATCH_ANY, name: $localize`Any`},
|
||||||
{id: MATCH_ALL, name: "All"},
|
{id: MATCH_ALL, name: $localize`All`},
|
||||||
{id: MATCH_LITERAL, name: "Literal"},
|
{id: MATCH_LITERAL, name: $localize`Literal`},
|
||||||
{id: MATCH_REGEX, name: "Regular Expression"},
|
{id: MATCH_REGEX, name: $localize`Regular expression`},
|
||||||
{id: MATCH_FUZZY, name: "Fuzzy Match"},
|
{id: MATCH_FUZZY, name: $localize`Fuzzy match`},
|
||||||
{id: MATCH_AUTO, name: "Auto"},
|
{id: MATCH_AUTO, name: $localize`Auto`},
|
||||||
]
|
]
|
||||||
|
|
||||||
export interface MatchingModel extends ObjectWithId {
|
export interface MatchingModel extends ObjectWithId {
|
||||||
@ -29,4 +29,6 @@ export interface MatchingModel extends ObjectWithId {
|
|||||||
|
|
||||||
is_insensitive?: boolean
|
is_insensitive?: boolean
|
||||||
|
|
||||||
|
document_count?: number
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { MatchingModel } from './matching-model';
|
import { MatchingModel } from './matching-model';
|
||||||
|
|
||||||
export interface PaperlessCorrespondent extends MatchingModel {
|
export interface PaperlessCorrespondent extends MatchingModel {
|
||||||
|
|
||||||
document_count?: number
|
|
||||||
|
|
||||||
last_correspondence?: Date
|
last_correspondence?: Date
|
||||||
|
|
||||||
|
@ -2,6 +2,4 @@ import { MatchingModel } from './matching-model';
|
|||||||
|
|
||||||
export interface PaperlessDocumentType extends MatchingModel {
|
export interface PaperlessDocumentType extends MatchingModel {
|
||||||
|
|
||||||
document_count?: number
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,19 +3,19 @@ import { ObjectWithId } from './object-with-id';
|
|||||||
|
|
||||||
|
|
||||||
export const TAG_COLOURS = [
|
export const TAG_COLOURS = [
|
||||||
{id: 1, value: "#a6cee3", name: "Light Blue", textColor: "#000000"},
|
{id: 1, value: "#a6cee3", name: $localize`Light blue`, textColor: "#000000"},
|
||||||
{id: 2, value: "#1f78b4", name: "Blue", textColor: "#ffffff"},
|
{id: 2, value: "#1f78b4", name: $localize`Blue`, textColor: "#ffffff"},
|
||||||
{id: 3, value: "#b2df8a", name: "Light Green", textColor: "#000000"},
|
{id: 3, value: "#b2df8a", name: $localize`Light green`, textColor: "#000000"},
|
||||||
{id: 4, value: "#33a02c", name: "Green", textColor: "#ffffff"},
|
{id: 4, value: "#33a02c", name: $localize`Green`, textColor: "#ffffff"},
|
||||||
{id: 5, value: "#fb9a99", name: "Light Red", textColor: "#000000"},
|
{id: 5, value: "#fb9a99", name: $localize`Light red`, textColor: "#000000"},
|
||||||
{id: 6, value: "#e31a1c", name: "Red ", textColor: "#ffffff"},
|
{id: 6, value: "#e31a1c", name: $localize`Red `, textColor: "#ffffff"},
|
||||||
{id: 7, value: "#fdbf6f", name: "Light Orange", textColor: "#000000"},
|
{id: 7, value: "#fdbf6f", name: $localize`Light orange`, textColor: "#000000"},
|
||||||
{id: 8, value: "#ff7f00", name: "Orange", textColor: "#000000"},
|
{id: 8, value: "#ff7f00", name: $localize`Orange`, textColor: "#000000"},
|
||||||
{id: 9, value: "#cab2d6", name: "Light Violet", textColor: "#000000"},
|
{id: 9, value: "#cab2d6", name: $localize`Light violet`, textColor: "#000000"},
|
||||||
{id: 10, value: "#6a3d9a", name: "Violet", textColor: "#ffffff"},
|
{id: 10, value: "#6a3d9a", name: $localize`Violet`, textColor: "#ffffff"},
|
||||||
{id: 11, value: "#b15928", name: "Brown", textColor: "#ffffff"},
|
{id: 11, value: "#b15928", name: $localize`Brown`, textColor: "#ffffff"},
|
||||||
{id: 12, value: "#000000", name: "Black", textColor: "#ffffff"},
|
{id: 12, value: "#000000", name: $localize`Black`, textColor: "#ffffff"},
|
||||||
{id: 13, value: "#cccccc", name: "Light Grey", textColor: "#000000"}
|
{id: 13, value: "#cccccc", name: $localize`Light grey`, textColor: "#000000"}
|
||||||
]
|
]
|
||||||
|
|
||||||
export interface PaperlessTag extends MatchingModel {
|
export interface PaperlessTag extends MatchingModel {
|
||||||
@ -23,6 +23,5 @@ export interface PaperlessTag extends MatchingModel {
|
|||||||
colour?: number
|
colour?: number
|
||||||
|
|
||||||
is_inbox_tag?: boolean
|
is_inbox_tag?: boolean
|
||||||
|
|
||||||
document_count?: number
|
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,3 @@ export const OPEN_DOCUMENT_SERVICE = {
|
|||||||
export const DOCUMENT_LIST_SERVICE = {
|
export const DOCUMENT_LIST_SERVICE = {
|
||||||
CURRENT_VIEW_CONFIG: 'document-list-service:currentViewConfig'
|
CURRENT_VIEW_CONFIG: 'document-list-service:currentViewConfig'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GENERAL_SETTINGS = {
|
|
||||||
DOCUMENT_LIST_SIZE: 'general-settings:documentListSize',
|
|
||||||
DOCUMENT_LIST_SIZE_DEFAULT: 50
|
|
||||||
}
|
|
@ -9,7 +9,7 @@ export class DocumentTitlePipe implements PipeTransform {
|
|||||||
if (value) {
|
if (value) {
|
||||||
return value
|
return value
|
||||||
} else {
|
} else {
|
||||||
return "(no title)"
|
return $localize`(no title)`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { Pipe, PipeTransform } from '@angular/core';
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
|
import { ToggleableItem } from 'src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component';
|
||||||
|
import { MatchingModel } from '../data/matching-model';
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'filter'
|
name: 'filter'
|
||||||
})
|
})
|
||||||
export class FilterPipe implements PipeTransform {
|
export class FilterPipe implements PipeTransform {
|
||||||
transform(items: any[], searchText: string): any[] {
|
transform(items: MatchingModel[], searchText: string): MatchingModel[] {
|
||||||
if (!items) return [];
|
if (!items) return [];
|
||||||
if (!searchText) return items;
|
if (!searchText) return items;
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import { Pipe, PipeTransform } from '@angular/core';
|
|||||||
export class YesNoPipe implements PipeTransform {
|
export class YesNoPipe implements PipeTransform {
|
||||||
|
|
||||||
transform(value: boolean): unknown {
|
transform(value: boolean): unknown {
|
||||||
return value ? "Yes" : "No"
|
return value ? $localize`Yes` : $localize`No`
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { cloneFilterRules, FilterRule } from '../data/filter-rule';
|
import { cloneFilterRules, FilterRule } from '../data/filter-rule';
|
||||||
import { PaperlessDocument } from '../data/paperless-document';
|
import { PaperlessDocument } from '../data/paperless-document';
|
||||||
import { PaperlessSavedView } from '../data/paperless-saved-view';
|
import { PaperlessSavedView } from '../data/paperless-saved-view';
|
||||||
import { DOCUMENT_LIST_SERVICE, GENERAL_SETTINGS } from '../data/storage-keys';
|
import { DOCUMENT_LIST_SERVICE } from '../data/storage-keys';
|
||||||
import { DocumentService } from './rest/document.service';
|
import { DocumentService } from './rest/document.service';
|
||||||
|
import { SettingsService, SETTINGS_KEYS } from './settings.service';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -23,7 +25,7 @@ export class DocumentListViewService {
|
|||||||
isReloading: boolean = false
|
isReloading: boolean = false
|
||||||
documents: PaperlessDocument[] = []
|
documents: PaperlessDocument[] = []
|
||||||
currentPage = 1
|
currentPage = 1
|
||||||
currentPageSize: number = +localStorage.getItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE) || GENERAL_SETTINGS.DOCUMENT_LIST_SIZE_DEFAULT
|
currentPageSize: number = this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)
|
||||||
collectionSize: number
|
collectionSize: number
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -154,6 +156,14 @@ export class DocumentListViewService {
|
|||||||
sessionStorage.setItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG, JSON.stringify(this.documentListView))
|
sessionStorage.setItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG, JSON.stringify(this.documentListView))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
quickFilter(filterRules: FilterRule[]) {
|
||||||
|
this.savedView = null
|
||||||
|
this.view.filter_rules = filterRules
|
||||||
|
this.reduceSelectionToFilter()
|
||||||
|
this.saveDocumentListView()
|
||||||
|
this.router.navigate(["documents"])
|
||||||
|
}
|
||||||
|
|
||||||
getLastPage(): number {
|
getLastPage(): number {
|
||||||
return Math.ceil(this.collectionSize / this.currentPageSize)
|
return Math.ceil(this.collectionSize / this.currentPageSize)
|
||||||
}
|
}
|
||||||
@ -190,7 +200,7 @@ export class DocumentListViewService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updatePageSize() {
|
updatePageSize() {
|
||||||
let newPageSize = +localStorage.getItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE) || GENERAL_SETTINGS.DOCUMENT_LIST_SIZE_DEFAULT
|
let newPageSize = this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)
|
||||||
if (newPageSize != this.currentPageSize) {
|
if (newPageSize != this.currentPageSize) {
|
||||||
this.currentPageSize = newPageSize
|
this.currentPageSize = newPageSize
|
||||||
}
|
}
|
||||||
@ -202,7 +212,7 @@ export class DocumentListViewService {
|
|||||||
this.selected.clear()
|
this.selected.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
private reduceSelectionToFilter() {
|
reduceSelectionToFilter() {
|
||||||
if (this.selected.size > 0) {
|
if (this.selected.size > 0) {
|
||||||
this.documentService.listAllFilteredIds(this.filterRules).subscribe(ids => {
|
this.documentService.listAllFilteredIds(this.filterRules).subscribe(ids => {
|
||||||
let subset = new Set<number>()
|
let subset = new Set<number>()
|
||||||
@ -239,7 +249,7 @@ export class DocumentListViewService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private documentService: DocumentService) {
|
constructor(private documentService: DocumentService, private settings: SettingsService, private router: Router) {
|
||||||
let documentListViewConfigJson = sessionStorage.getItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG)
|
let documentListViewConfigJson = sessionStorage.getItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG)
|
||||||
if (documentListViewConfigJson) {
|
if (documentListViewConfigJson) {
|
||||||
try {
|
try {
|
||||||
|
@ -28,6 +28,9 @@ export class OpenDocumentsService {
|
|||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
this.documentService.get(id).subscribe(doc => {
|
this.documentService.get(id).subscribe(doc => {
|
||||||
this.openDocuments[index] = doc
|
this.openDocuments[index] = doc
|
||||||
|
}, error => {
|
||||||
|
this.openDocuments.splice(index, 1)
|
||||||
|
this.save()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,27 +74,31 @@ export abstract class AbstractPaperlessService<T extends ObjectWithId> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearCache() {
|
||||||
|
this._listAll = null
|
||||||
|
}
|
||||||
|
|
||||||
get(id: number): Observable<T> {
|
get(id: number): Observable<T> {
|
||||||
return this.http.get<T>(this.getResourceUrl(id))
|
return this.http.get<T>(this.getResourceUrl(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
create(o: T): Observable<T> {
|
create(o: T): Observable<T> {
|
||||||
this._listAll = null
|
this.clearCache()
|
||||||
return this.http.post<T>(this.getResourceUrl(), o)
|
return this.http.post<T>(this.getResourceUrl(), o)
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(o: T): Observable<any> {
|
delete(o: T): Observable<any> {
|
||||||
this._listAll = null
|
this.clearCache()
|
||||||
return this.http.delete(this.getResourceUrl(o.id))
|
return this.http.delete(this.getResourceUrl(o.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
update(o: T): Observable<T> {
|
update(o: T): Observable<T> {
|
||||||
this._listAll = null
|
this.clearCache()
|
||||||
return this.http.put<T>(this.getResourceUrl(o.id), o)
|
return this.http.put<T>(this.getResourceUrl(o.id), o)
|
||||||
}
|
}
|
||||||
|
|
||||||
patch(o: T): Observable<T> {
|
patch(o: T): Observable<T> {
|
||||||
this._listAll = null
|
this.clearCache()
|
||||||
return this.http.patch<T>(this.getResourceUrl(o.id), o)
|
return this.http.patch<T>(this.getResourceUrl(o.id), o)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { PaperlessDocument } from 'src/app/data/paperless-document';
|
import { PaperlessDocument } from 'src/app/data/paperless-document';
|
||||||
import { PaperlessDocumentMetadata } from 'src/app/data/paperless-document-metadata';
|
import { PaperlessDocumentMetadata } from 'src/app/data/paperless-document-metadata';
|
||||||
import { AbstractPaperlessService } from './abstract-paperless-service';
|
import { AbstractPaperlessService } from './abstract-paperless-service';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { Results } from 'src/app/data/results';
|
import { Results } from 'src/app/data/results';
|
||||||
import { FilterRule } from 'src/app/data/filter-rule';
|
import { FilterRule } from 'src/app/data/filter-rule';
|
||||||
@ -13,15 +13,26 @@ import { TagService } from './tag.service';
|
|||||||
import { FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type';
|
import { FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type';
|
||||||
|
|
||||||
export const DOCUMENT_SORT_FIELDS = [
|
export const DOCUMENT_SORT_FIELDS = [
|
||||||
{ field: "correspondent__name", name: "Correspondent" },
|
{ field: "correspondent__name", name: $localize`Correspondent` },
|
||||||
{ field: "document_type__name", name: "Document type" },
|
{ field: "document_type__name", name: $localize`Document type` },
|
||||||
{ field: 'title', name: 'Title' },
|
{ field: 'title', name: $localize`Title` },
|
||||||
{ field: 'archive_serial_number', name: 'ASN' },
|
{ field: 'archive_serial_number', name: $localize`ASN` },
|
||||||
{ field: 'created', name: 'Created' },
|
{ field: 'created', name: $localize`Created` },
|
||||||
{ field: 'added', name: 'Added' },
|
{ field: 'added', name: $localize`Added` },
|
||||||
{ field: 'modified', name: 'Modified' }
|
{ field: 'modified', name: $localize`Modified` }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export interface SelectionDataItem {
|
||||||
|
id: number
|
||||||
|
document_count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectionData {
|
||||||
|
selected_correspondents: SelectionDataItem[]
|
||||||
|
selected_tags: SelectionDataItem[]
|
||||||
|
selected_document_types: SelectionDataItem[]
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
@ -38,6 +49,8 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument>
|
|||||||
let ruleType = FILTER_RULE_TYPES.find(t => t.id == rule.rule_type)
|
let ruleType = FILTER_RULE_TYPES.find(t => t.id == rule.rule_type)
|
||||||
if (ruleType.multi) {
|
if (ruleType.multi) {
|
||||||
params[ruleType.filtervar] = params[ruleType.filtervar] ? params[ruleType.filtervar] + "," + rule.value : rule.value
|
params[ruleType.filtervar] = params[ruleType.filtervar] ? params[ruleType.filtervar] + "," + rule.value : rule.value
|
||||||
|
} else if (ruleType.isnull_filtervar && rule.value == null) {
|
||||||
|
params[ruleType.isnull_filtervar] = true
|
||||||
} else {
|
} else {
|
||||||
params[ruleType.filtervar] = rule.value
|
params[ruleType.filtervar] = rule.value
|
||||||
}
|
}
|
||||||
@ -112,4 +125,8 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSelectionData(ids: number[]): Observable<SelectionData> {
|
||||||
|
return this.http.post<SelectionData>(this.getResourceUrl(null, 'selection_data'), {"documents": ids})
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
16
src-ui/src/app/services/settings.service.spec.ts
Normal file
16
src-ui/src/app/services/settings.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SettingsService } from './settings.service';
|
||||||
|
|
||||||
|
describe('SettingsService', () => {
|
||||||
|
let service: SettingsService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(SettingsService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
60
src-ui/src/app/services/settings.service.ts
Normal file
60
src-ui/src/app/services/settings.service.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
export interface PaperlessSettings {
|
||||||
|
key: string
|
||||||
|
type: string
|
||||||
|
default: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SETTINGS_KEYS = {
|
||||||
|
BULK_EDIT_CONFIRMATION_DIALOGS: 'general-settings:bulk-edit:confirmation-dialogs',
|
||||||
|
BULK_EDIT_APPLY_ON_CLOSE: 'general-settings:bulk-edit:apply-on-close',
|
||||||
|
DOCUMENT_LIST_SIZE: 'general-settings:documentListSize',
|
||||||
|
}
|
||||||
|
|
||||||
|
const SETTINGS: PaperlessSettings[] = [
|
||||||
|
{key: SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS, type: "boolean", default: true},
|
||||||
|
{key: SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE, type: "boolean", default: false},
|
||||||
|
{key: SETTINGS_KEYS.DOCUMENT_LIST_SIZE, type: "number", default: 50}
|
||||||
|
]
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class SettingsService {
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
get(key: string): any {
|
||||||
|
let setting = SETTINGS.find(s => s.key == key)
|
||||||
|
|
||||||
|
if (!setting) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = localStorage.getItem(key)
|
||||||
|
|
||||||
|
if (value != null) {
|
||||||
|
switch (setting.type) {
|
||||||
|
case "boolean":
|
||||||
|
return JSON.parse(value)
|
||||||
|
case "number":
|
||||||
|
return +value
|
||||||
|
case "string":
|
||||||
|
return value
|
||||||
|
default:
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return setting.default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key: string, value: any) {
|
||||||
|
localStorage.setItem(key, value.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
unset(key: string) {
|
||||||
|
localStorage.removeItem(key)
|
||||||
|
}
|
||||||
|
}
|
@ -1,30 +1,13 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Subject, zip } from 'rxjs';
|
import { Subject, zip } from 'rxjs';
|
||||||
|
|
||||||
export class Toast {
|
export interface Toast {
|
||||||
|
|
||||||
static make(title: string, content: string, classname?: string, delay?: number): Toast {
|
|
||||||
let t = new Toast()
|
|
||||||
t.title = title
|
|
||||||
t.content = content
|
|
||||||
t.classname = classname
|
|
||||||
if (delay) {
|
|
||||||
t.delay = delay
|
|
||||||
}
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
static makeError(content: string) {
|
|
||||||
return Toast.make("Error", content, null, 10000)
|
|
||||||
}
|
|
||||||
|
|
||||||
title: string
|
title: string
|
||||||
|
|
||||||
classname: string
|
|
||||||
|
|
||||||
content: string
|
content: string
|
||||||
|
|
||||||
delay: number = 5000
|
delay: number
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,11 +22,19 @@ export class ToastService {
|
|||||||
|
|
||||||
private toastsSubject: Subject<Toast[]> = new Subject()
|
private toastsSubject: Subject<Toast[]> = new Subject()
|
||||||
|
|
||||||
showToast(toast: Toast) {
|
show(toast: Toast) {
|
||||||
this.toasts.push(toast)
|
this.toasts.push(toast)
|
||||||
this.toastsSubject.next(this.toasts)
|
this.toastsSubject.next(this.toasts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showError(content: string, delay: number = 10000) {
|
||||||
|
this.show({title: $localize`Error`, content: content, delay: delay})
|
||||||
|
}
|
||||||
|
|
||||||
|
showInfo(content: string, delay: number = 5000) {
|
||||||
|
this.show({title: $localize`Information`, content: content, delay: delay})
|
||||||
|
}
|
||||||
|
|
||||||
closeToast(toast: Toast) {
|
closeToast(toast: Toast) {
|
||||||
let index = this.toasts.findIndex(t => t == toast)
|
let index = this.toasts.findIndex(t => t == toast)
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
|
@ -2,5 +2,5 @@ export const environment = {
|
|||||||
production: true,
|
production: true,
|
||||||
apiBaseUrl: "/api/",
|
apiBaseUrl: "/api/",
|
||||||
appTitle: "Paperless-ng",
|
appTitle: "Paperless-ng",
|
||||||
version: "0.9.9"
|
version: "0.9.10"
|
||||||
};
|
};
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
<title>Paperless-ng</title>
|
<title>Paperless-ng</title>
|
||||||
<base href="/">
|
<base href="/">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="theme-color" content="#17541f" />
|
||||||
|
<link rel="manifest" href="manifest.webmanifest">
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
14
src-ui/src/manifest.webmanifest
Normal file
14
src-ui/src/manifest.webmanifest
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"background_color": "white",
|
||||||
|
"description": "A supercharged version of paperless: scan, index and archive all your physical documents",
|
||||||
|
"display": "fullscreen",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "128x128"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "Paperless NG",
|
||||||
|
"short_name": "Paperless NG",
|
||||||
|
"start_url": "/"
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user