-
Correspondent:
+
-
-
Document Type:
+
-
@@ -71,19 +71,19 @@
- Add
+
Add
- Edit
+ Edit
- Remove
+ Remove
diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.scss b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.scss
index 3868e7a02..5afd86545 100644
--- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.scss
+++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.scss
@@ -1,6 +1,10 @@
.btn svg {
width: 0.9em;
height: 0.9em;
- margin-right: 2px;
+ margin-right: 3px;
margin-top: -1px;
}
+
+.btn-sm {
+ line-height: 1;
+}
From fb9d750684092f40b4d5f5cee565d12ecd15f2f5 Mon Sep 17 00:00:00 2001
From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com>
Date: Tue, 15 Dec 2020 14:19:40 -0800
Subject: [PATCH 06/73] Delete button margin-left
---
.../document-list/bulk-editor/bulk-editor.component.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html
index 22724db17..d330ba228 100644
--- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html
+++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html
@@ -56,7 +56,7 @@
+
From fbb2da42dc92cabe2379b890e8c3ea0a1fb7d591 Mon Sep 17 00:00:00 2001
From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com>
Date: Fri, 18 Dec 2020 14:55:21 -0800
Subject: [PATCH 07/73] Merge branch 'dev' into feature-bulk-editor
---
README.md | 25 ++-
docker/hub/docker-compose.postgres.yml | 2 +-
docker/hub/docker-compose.sqlite.yml | 2 +-
docs/advanced_usage.rst | 36 ++--
docs/changelog.rst | 54 +++++
docs/usage_overview.rst | 14 +-
scripts/make-release.sh | 1 +
src-ui/package-lock.json | 8 +
src-ui/package.json | 1 +
src-ui/src/app/app.module.ts | 7 +-
.../app-frame/app-frame.component.html | 7 +-
.../app-frame/app-frame.component.ts | 3 +
.../common/input/select/select.component.html | 18 +-
.../common/input/select/select.component.scss | 1 +
.../common/input/tags/tags.component.html | 45 ++--
.../common/input/tags/tags.component.scss | 16 +-
.../common/input/tags/tags.component.ts | 23 +-
.../saved-view-widget.component.html | 2 +-
.../document-detail.component.html | 8 +-
.../document-detail.component.ts | 6 +-
.../document-card-large.component.scss | 1 +
.../document-card-small.component.html | 4 +-
.../document-list.component.html | 6 +-
.../document-list.component.scss | 24 ++-
.../document-list/document-list.component.ts | 1 +
.../save-view-config-dialog.component.ts | 15 +-
.../filter-dropdown-date.component.html | 51 ++---
.../filter-dropdown-date.component.ts | 136 ++++++------
.../filter-dropdown.component.html | 12 +-
.../filter-dropdown.component.scss | 6 +
.../filter-dropdown.component.ts | 2 +-
.../filter-editor.component.html | 45 ++--
.../filter-editor/filter-editor.component.ts | 64 +++---
.../correspondent-list.component.html | 23 +-
.../correspondent-list.component.ts | 14 +-
.../document-type-list.component.html | 21 +-
.../document-type-list.component.ts | 14 +-
.../generic-list/generic-list.component.ts | 2 +-
.../manage/settings/settings.component.ts | 20 +-
.../manage/tag-list/tag-list.component.html | 23 +-
.../manage/tag-list/tag-list.component.ts | 15 +-
.../src/app/interceptors/csrf.interceptor.ts | 9 +-
src-ui/src/app/pipes/document-title.pipe.ts | 2 +-
.../services/document-list-view.service.ts | 6 +-
src-ui/src/assets/save-filter.png | Bin 8267 -> 8263 bytes
src-ui/src/environments/environment.prod.ts | 3 +-
src-ui/src/styles.scss | 42 +++-
src/documents/admin.py | 2 +-
src/documents/checks.py | 3 +-
src/documents/file_handling.py | 7 +-
.../management/commands/decrypt_documents.py | 13 +-
src/documents/migrations/1003_mime_types.py | 17 +-
.../migrations/1008_auto_20201216_1736.py | 34 +++
.../migrations/1009_auto_20201216_2005.py | 29 +++
src/documents/models.py | 15 +-
src/documents/parsers.py | 6 +-
src/documents/serialisers.py | 8 +-
src/documents/templates/index.html | 1 +
src/documents/tests/test_api.py | 203 ++++++++----------
src/documents/tests/test_file_handling.py | 11 +-
src/documents/tests/test_index.py | 21 ++
src/documents/tests/test_sanity_check.py | 10 +-
src/documents/tests/utils.py | 3 +-
src/documents/views.py | 19 +-
src/paperless/version.py | 2 +-
src/paperless_mail/mail.py | 10 +-
src/paperless_mail/tests/test_mail.py | 4 +-
src/paperless_text/parsers.py | 62 +-----
src/paperless_text/tests/samples/test.txt | 1 +
src/paperless_text/tests/test_parser.py | 26 +++
70 files changed, 874 insertions(+), 473 deletions(-)
create mode 100644 src/documents/migrations/1008_auto_20201216_1736.py
create mode 100644 src/documents/migrations/1009_auto_20201216_2005.py
create mode 100644 src/paperless_text/tests/samples/test.txt
create mode 100644 src/paperless_text/tests/test_parser.py
diff --git a/README.md b/README.md
index 41f85af19..e8ae8feb2 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
[Paperless](https://github.com/the-paperless-project/paperless) is an application by Daniel Quinn and others that indexes your scanned documents and allows you to easily search for documents and store metadata alongside your documents.
-Paperless-ng is a fork of the original project, adding a new interface and many other changes under the hood. For a detailed list of changes, see below.
+Paperless-ng is a fork of the original project, adding a new interface and many other changes under the hood. For a detailed list of changes, have a look at the changelog in the documentation.
This project is still in development and some things may not work as expected.
@@ -15,11 +15,13 @@ This project is still in development and some things may not work as expected.
Paperless does not control your scanner, it only helps you deal with what your scanner produces.
-1. Buy a document scanner that can write to a place on your network. If you need some inspiration, have a look at the [scanner recommendations](https://paperless-ng.readthedocs.io/en/latest/scanners.html) page.
-2. Set it up to "scan to FTP" or something similar. It should be able to push scanned images to a server without you having to do anything. Of course if your scanner doesn't know how to automatically upload the file somewhere, you can always do that manually. Paperless doesn't care how the documents get into its local consumption directory.
-3. Have the target server run the Paperless consumption script to OCR the file and index it into a local database.
-4. Use the web frontend to sift through the database and find what you want.
-5. Download the PDF you need/want via the web interface and do whatever you like with it. You can even print it and send it as if it's the original. In most cases, no one will care or notice.
+1. Buy a document scanner that can write to a place on your network. If you need some inspiration, have a look at the [scanner recommendations](https://paperless-ng.readthedocs.io/en/latest/scanners.html) page. Set it up to "scan to FTP" or something similar. It should be able to push scanned images to a server without you having to do anything. Of course if your scanner doesn't know how to automatically upload the file somewhere, you can always do that manually. Paperless doesn't care how the documents get into its local consumption directory.
+
+ - Alternatively, you can use any of the mobile scanning apps out there. We have an app that allows you to share documents with paperless, if you're on Android. See the section on affiliated projects.
+
+2. Wait for paperless to process your files. OCR is expensive, and depending on the power of your machine, this might take a bit of time.
+3. Use the web frontend to sift through the database and find what you want.
+4. Download the PDF you need/want via the web interface and do whatever you like with it. You can even print it and send it as if it's the original. In most cases, no one will care or notice.
Here's what you get:
@@ -39,7 +41,6 @@ Here's what you get:
* When adding documents from mails, paperless can move these mails to a new folder, mark them as read, flag them or delete them.
* Machine learning powered document matching.
* Paperless learns from your documents and will be able to automatically assign tags, correspondents and types to documents once you've stored a few documents in paperless.
-* We have a mobile app that offers a 'Share with paperless' option over at https://github.com/qcasey/paperless_share. You can use that in combination with any of the mobile scanning apps out there. It's still a little rough around the edges, but it works!
* A task processor that processes documents in parallel and also tells you when something goes wrong. On modern multi core systems, consumption is blazing fast.
* Code cleanup in many, MANY areas. Some of the code from OG paperless was just overly complicated.
* More tests, more stability.
@@ -78,7 +79,7 @@ The recommended way to deploy paperless is docker-compose. Don't clone the repos
Read the [documentation](https://paperless-ng.readthedocs.io/en/latest/setup.html#installation) on how to get started.
-Alternatively, you can install the dependencies and setup apache and a database server yourself. The documenation has information about the individual components of paperless that you need to take care of.
+Alternatively, you can install the dependencies and setup apache and a database server yourself. The documenation has a step by step guide on how to do it.
# Migrating to paperless-ng
@@ -102,13 +103,15 @@ If you want to implement something big: Please start a discussion about that in
Paperless has been around a while now, and people are starting to build stuff on top of it. If you're one of those people, we can add your project to this list:
-* [Paperless App](https://github.com/bauerj/paperless_app): An Android/iOS app for Paperless. We're working on making this compatible.
+* [Paperless App](https://github.com/bauerj/paperless_app): An Android/iOS app for Paperless. Updated to work with paperless-ng.
+* [Paperless Share](https://github.com/qcasey/paperless_share). Share any files from your Android application with paperless. Very simple, but works with all of the mobile scanning apps out there that allow you to share scanned documents.
+
+These projects also exist, but their status and compatibility with paperless-ng is unknown.
+
* [Paperless Desktop](https://github.com/thomasbrueggemann/paperless-desktop): A desktop UI for your Paperless installation. Runs on Mac, Linux, and Windows.
* [ansible-role-paperless](https://github.com/ovv/ansible-role-paperless): An easy way to get Paperless running via Ansible.
* [paperless-cli](https://github.com/stgarf/paperless-cli): A golang command line binary to interact with a Paperless instance.
-Compatibility with Paperless-ng is unknown.
-
# Important Note
Document scanners are typically used to scan sensitive documents. Things like your social insurance number, tax records, invoices, etc. Everything is stored in the clear without encryption by default (it needs to be searchable, so if someone has ideas on how to do that on encrypted data, I'm all ears). This means that Paperless should never be run on an untrusted host. Instead, I recommend that if you do want to use it, run it locally on a server in your own home.
diff --git a/docker/hub/docker-compose.postgres.yml b/docker/hub/docker-compose.postgres.yml
index 24f0e118f..d33e4c38d 100644
--- a/docker/hub/docker-compose.postgres.yml
+++ b/docker/hub/docker-compose.postgres.yml
@@ -15,7 +15,7 @@ services:
POSTGRES_PASSWORD: paperless
webserver:
- image: jonaswinkler/paperless-ng:0.9.6
+ image: jonaswinkler/paperless-ng:0.9.8
restart: always
depends_on:
- db
diff --git a/docker/hub/docker-compose.sqlite.yml b/docker/hub/docker-compose.sqlite.yml
index 6ae619fd6..c130dfef6 100644
--- a/docker/hub/docker-compose.sqlite.yml
+++ b/docker/hub/docker-compose.sqlite.yml
@@ -5,7 +5,7 @@ services:
restart: always
webserver:
- image: jonaswinkler/paperless-ng:0.9.6
+ image: jonaswinkler/paperless-ng:0.9.8
restart: always
depends_on:
- broker
diff --git a/docs/advanced_usage.rst b/docs/advanced_usage.rst
index b5ae254b3..48a86384c 100644
--- a/docs/advanced_usage.rst
+++ b/docs/advanced_usage.rst
@@ -263,10 +263,10 @@ using the identifier which it has assigned to each document. You will end up get
files like ``0000123.pdf`` in your media directory. This isn't necessarily a bad
thing, because you normally don't have to access these files manually. However, if
you wish to name your files differently, you can do that by adjusting the
-``PAPERLESS_FILENAME_FORMAT`` settings variable.
+``PAPERLESS_FILENAME_FORMAT`` configuration option.
-This variable allows you to configure the filename (folders are allowed!) using
-placeholders. For example, setting
+This variable allows you to configure the filename (folders are allowed) using
+placeholders. For example, configuring this to
.. code:: bash
@@ -277,17 +277,16 @@ will create a directory structure as follows:
.. code::
2019/
- my_bank/
- statement-january-0000001.pdf
- statement-february-0000002.pdf
+ My bank/
+ Statement January.pdf
+ Statement February.pdf
2020/
- my_bank/
- statement-january-0000003.pdf
- shoe_store/
- my_new_shoes-0000004.pdf
-
-Paperless appends the unique identifier of each document to the filename. This
-avoids filename clashes.
+ My bank/
+ Statement January.pdf
+ Letter.pdf
+ Letter_01.pdf
+ Shoe store/
+ My new shoes.pdf
.. danger::
@@ -299,6 +298,7 @@ Paperless provides the following placeholders withing filenames:
* ``{correspondent}``: The name of the correspondent, or "none".
* ``{document_type}``: The name of the document type, or "none".
+* ``{tag_list}``: A comma separated list of all tags assigned to the document.
* ``{title}``: The title of the document.
* ``{created}``: The full date and time the document was created.
* ``{created_year}``: Year created only.
@@ -309,8 +309,14 @@ Paperless provides the following placeholders withing filenames:
* ``{added_month}``: Month added only (number 1-12).
* ``{added_day}``: Day added only (number 1-31).
-Paperless will convert all values for the placeholders into values which are safe
-for use in filenames.
+
+Paperless will try to conserve the information from your database as much as possible.
+However, some characters that you can use in document titles and correspondent names (such
+as ``: \ /`` and a couple more) are not allowed in filenames and will be replaced with dashes.
+
+If paperless detects that two documents share the same filename, paperless will automatically
+append ``_01``, ``_02``, etc to the filename. This happens if all the placeholders in a filename
+evaluate to the same value.
.. hint::
diff --git a/docs/changelog.rst b/docs/changelog.rst
index a50fc31d5..a993eb530 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -5,6 +5,58 @@
Changelog
*********
+
+paperless-ng 0.9.8
+##################
+
+This release addresses two severe issues with the previous release.
+
+* The delete buttons for document types, correspondents and tags were not working.
+* The document section in the admin was causing internal server errors (500).
+
+
+paperless-ng 0.9.7
+##################
+
+
+* Front end
+
+ * Thanks to the hard work of `Michael Shamoon`_, paperless now comes with a much more streamlined UI for
+ filtering documents.
+
+ * `Michael Shamoon`_ replaced the document preview with another component. This should fix compatibility with Safari browsers.
+
+ * Added buttons to the management pages to quickly show all documents with one specific tag, correspondent, or title.
+
+ * Paperless now stores your saved views on the server and associates them with your user account.
+ This means that you can access your views on multiple devices and have separate views for different users.
+ You will have to recreate your views.
+
+ * The GitHub and documentation links now open in new tabs/windows. Thanks to `rYR79435`_.
+
+ * Paperless now generates default saved view names when saving views with certain filter rules.
+
+ * Added a small version indicator to the front end.
+
+* Other additions and changes
+
+ * The new filename format field ``{tag_list}`` inserts a list of tags into the filename, separated by comma.
+ * The ``document_retagger`` no longer removes inbox tags or tags without matching rules.
+ * The new configuration option ``PAPERLESS_COOKIE_PREFIX`` allows you to run multiple instances of paperless on different ports.
+ This option enables you to be logged in into multiple instances by specifying different cookie names for each instance.
+
+* Fixes
+
+ * Sometimes paperless would assign dates in the future to newly consumed documents.
+ * The filename format fields ``{created_month}`` and ``{created_day}`` now use a leading zero for single digit values.
+ * The filename format field ``{tags}`` can no longer be used without arguments.
+ * Paperless was not able to consume many images (especially images from mobile scanners) due to missing DPI information.
+ Paperless now assumes A4 paper size for PDF generation if no DPI information is present.
+ * Documents with empty titles could not be opened from the table view due to the link being empty.
+ * Fixed an issue with filenames containing special characters such as ``:`` not being accepted for upload.
+ * Fixed issues with thumbnail generation for plain text files.
+
+
paperless-ng 0.9.6
##################
@@ -841,6 +893,8 @@ bulk of the work on this big change.
* Initial release
+.. _rYR79435: https://github.com/rYR79435
+.. _Michael Shamoon: https://github.com/shamoon
.. _jayme-github: http://github.com/jayme-github
.. _Brian Conn: https://github.com/TheConnMan
.. _Christopher Luu: https://github.com/nuudles
diff --git a/docs/usage_overview.rst b/docs/usage_overview.rst
index bb9ecd452..7a4fd7740 100644
--- a/docs/usage_overview.rst
+++ b/docs/usage_overview.rst
@@ -57,9 +57,6 @@ Adding documents to paperless
#############################
Once you've got Paperless setup, you need to start feeding documents into it.
-Currently, there are four options: the consumption directory, the dashboard, IMAP (email), and
-HTTP POST.
-
When adding documents to paperless, it will perform the following operations on
your documents:
@@ -112,6 +109,17 @@ Dashboard upload
The dashboard has a file drop field to upload documents to paperless. Simply drag a file
onto this field or select a file with the file dialog. Multiple files are supported.
+
+Mobile upload
+=============
+
+The mobile app over at ``_ allows Android users
+to share any documents with paperless. This can be combined with any of the mobile
+scanning apps out there, such as Office Lens.
+
+Furthermore, there is the `Paperless App `_ as well,
+which no only has document upload, but also document editing and browsing.
+
.. _usage-email:
IMAP (Email)
diff --git a/scripts/make-release.sh b/scripts/make-release.sh
index 0a7bc7a9b..f5c9028fa 100755
--- a/scripts/make-release.sh
+++ b/scripts/make-release.sh
@@ -5,6 +5,7 @@
# adjust src/paperless/version.py
# changelog in the documentation
# adjust versions in docker/hub/*
+# adjust version in src-ui/src/environments/prod
# If docker-compose was modified: all compose files are the same.
# Steps:
diff --git a/src-ui/package-lock.json b/src-ui/package-lock.json
index 5eca0b3c0..10215a32d 100644
--- a/src-ui/package-lock.json
+++ b/src-ui/package-lock.json
@@ -2056,6 +2056,14 @@
"tslib": "^2.0.0"
}
},
+ "@ng-select/ng-select": {
+ "version": "5.0.9",
+ "resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-5.0.9.tgz",
+ "integrity": "sha512-YZeSAiS8/Nx/eHZJPmOOYL8YmcvSq+dr1P8WIrsKmRA7mueorBpPc5xlUj+nLQbpLtsiQvdWDQspf/ykOvD/lA==",
+ "requires": {
+ "tslib": "^2.0.0"
+ }
+ },
"@ngtools/webpack": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-10.2.0.tgz",
diff --git a/src-ui/package.json b/src-ui/package.json
index 6293f2672..14d828483 100644
--- a/src-ui/package.json
+++ b/src-ui/package.json
@@ -21,6 +21,7 @@
"@angular/platform-browser-dynamic": "~10.1.5",
"@angular/router": "~10.1.5",
"@ng-bootstrap/ng-bootstrap": "^8.0.0",
+ "@ng-select/ng-select": "^5.0.9",
"bootstrap": "^4.5.0",
"ng-bootstrap": "^1.6.3",
"ng2-pdf-viewer": "^6.3.2",
diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts
index 627d4f6cf..6c4cabe92 100644
--- a/src-ui/src/app/app.module.ts
+++ b/src-ui/src/app/app.module.ts
@@ -56,6 +56,7 @@ import { FilterPipe } from './pipes/filter.pipe';
import { DocumentTitlePipe } from './pipes/document-title.pipe';
import { MetadataCollapseComponent } from './components/document-detail/metadata-collapse/metadata-collapse.component';
import { SelectDialogComponent } from './components/common/select-dialog/select-dialog.component';
+import { NgSelectModule } from '@ng-select/ng-select';
@NgModule({
declarations: [
@@ -114,7 +115,8 @@ import { SelectDialogComponent } from './components/common/select-dialog/select-
ReactiveFormsModule,
NgxFileDropModule,
InfiniteScrollModule,
- PdfViewerModule
+ PdfViewerModule,
+ NgSelectModule
],
providers: [
DatePipe,
@@ -123,7 +125,8 @@ import { SelectDialogComponent } from './components/common/select-dialog/select-
useClass: CsrfInterceptor,
multi: true
},
- FilterPipe
+ FilterPipe,
+ DocumentTitlePipe
],
bootstrap: [AppComponent]
})
diff --git a/src-ui/src/app/components/app-frame/app-frame.component.html b/src-ui/src/app/components/app-frame/app-frame.component.html
index 7876150af..2458005f4 100644
--- a/src-ui/src/app/components/app-frame/app-frame.component.html
+++ b/src-ui/src/app/components/app-frame/app-frame.component.html
@@ -17,6 +17,11 @@
-
{{toggleableItem?.count}}
+
{{toggleableItem?.count}}
diff --git a/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts b/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts
index adfea556a..400029615 100644
--- a/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts
+++ b/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts
@@ -23,9 +23,6 @@ export class ToggleableDropdownButtonComponent {
@Input()
toggleableItem: ToggleableItem
- @Input()
- showCounts: boolean = true
-
@Output()
toggle = new EventEmitter()
diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html
index aa85fdb12..fd619717f 100644
--- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html
+++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html
@@ -32,21 +32,18 @@
From a283b815ef3f07be46af85ff18aa5f9f00df60dd Mon Sep 17 00:00:00 2001
From: jonaswinkler
Date: Sun, 27 Dec 2020 13:34:36 +0100
Subject: [PATCH 51/73] refactor
---
.../toggleable-dropdown-button.component.ts | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts b/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts
index 400029615..cffbc916f 100644
--- a/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts
+++ b/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts
@@ -36,9 +36,12 @@ export class ToggleableDropdownButtonComponent {
}
getSelectedIconName() {
- let iconName = ''
- if (this.toggleableItem?.state == ToggleableItemState.Selected) iconName = 'check'
- else if (this.toggleableItem?.state == ToggleableItemState.PartiallySelected) iconName = 'dash'
- return iconName
+ if (this.toggleableItem?.state == ToggleableItemState.Selected) {
+ return "check"
+ } else if (this.toggleableItem?.state == ToggleableItemState.PartiallySelected) {
+ return "dash"
+ } else {
+ return ""
+ }
}
}
From c2a47ca4b10c283868dfd8e9ced9af971a0e1abf Mon Sep 17 00:00:00 2001
From: jonaswinkler
Date: Sun, 27 Dec 2020 13:34:54 +0100
Subject: [PATCH 52/73] remove "selectionSpansPages"
---
.../document-list/bulk-editor/bulk-editor.component.ts | 9 +--------
1 file changed, 1 insertion(+), 8 deletions(-)
diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts
index a5732b922..4365e00b0 100644
--- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts
+++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts
@@ -39,16 +39,11 @@ export class BulkEditorComponent {
dropdownTypes = FilterableDropdownType
- get selectionSpansPages(): boolean {
- return this.documentList.selected.size > this.documentList.documents.length || !Array.from(this.documentList.selected).every(sd => this.documentList.documents.find(d => d.id == sd))
- }
-
private _tagsToggleableItems: ToggleableItem[]
get tagsToggleableItems(): ToggleableItem[] {
let tagsToggleableItems = []
let selectedDocuments: PaperlessDocument[] = this.documentList.documents.filter(d => this.documentList.selected.has(d.id))
- if (this.selectionSpansPages) selectedDocuments = []
-
+
this.tags?.forEach(t => {
let selectedDocumentsWithTag: PaperlessDocument[] = selectedDocuments.filter(d => d.tags.includes(t.id))
let state = ToggleableItemState.NotSelected
@@ -64,7 +59,6 @@ export class BulkEditorComponent {
get correspondentsToggleableItems(): ToggleableItem[] {
let correspondentsToggleableItems = []
let selectedDocuments: PaperlessDocument[] = this.documentList.documents.filter(d => this.documentList.selected.has(d.id))
- if (this.selectionSpansPages) selectedDocuments = []
this.correspondents?.forEach(c => {
let selectedDocumentsWithCorrespondent: PaperlessDocument[] = selectedDocuments.filter(d => d.correspondent == c.id)
@@ -81,7 +75,6 @@ export class BulkEditorComponent {
get documentTypesToggleableItems(): ToggleableItem[] {
let documentTypesToggleableItems = []
let selectedDocuments: PaperlessDocument[] = this.documentList.documents.filter(d => this.documentList.selected.has(d.id))
- if (this.selectionSpansPages) selectedDocuments = []
this.documentTypes?.forEach(dt => {
let selectedDocumentsWithDocumentType: PaperlessDocument[] = selectedDocuments.filter(d => d.document_type == dt.id)
From 131ebf0480d88b6f6363785b42e0ae3259495992 Mon Sep 17 00:00:00 2001
From: Jonas Winkler
Date: Sun, 27 Dec 2020 17:07:33 +0100
Subject: [PATCH 53/73] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 7a5f6028d..7f3808905 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
[](https://paperless-ng.readthedocs.io/en/latest/?badge=latest)
[](https://gitter.im/paperless-ng/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[](https://hub.docker.com/r/jonaswinkler/paperless-ng)
-[](https://coveralls.io/github/jonaswinkler/paperless-ng?branch=master) *<-- green badge, yay :) *
+[](https://coveralls.io/github/jonaswinkler/paperless-ng?branch=master)
# Paperless-ng
From b8e7506de42723538faac6aab528a0b17ad8290f Mon Sep 17 00:00:00 2001
From: jonaswinkler
Date: Sun, 27 Dec 2020 23:55:19 +0100
Subject: [PATCH 54/73] partial selection model implementation
---
.../date-dropdown/date-dropdown.component.ts | 12 +-
.../filterable-dropdown.component.html | 12 +-
.../filterable-dropdown.component.ts | 145 ++++++++----
.../toggleable-dropdown-button.component.ts | 3 +-
.../bulk-editor/bulk-editor.component.html | 4 +-
.../filter-editor.component.html | 10 +-
.../filter-editor/filter-editor.component.ts | 222 +++++++-----------
7 files changed, 199 insertions(+), 209 deletions(-)
diff --git a/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.ts b/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.ts
index e787ce11e..1bf5d0216 100644
--- a/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.ts
+++ b/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.ts
@@ -30,9 +30,15 @@ export class DateDropdownComponent implements OnInit, OnDestroy {
@Input()
dateBefore: string
+ @Output()
+ dateBeforeChange = new EventEmitter()
+
@Input()
dateAfter: string
+ @Output()
+ dateAfterChange = new EventEmitter()
+
@Input()
title: string
@@ -83,6 +89,8 @@ export class DateDropdownComponent implements OnInit, OnDestroy {
}
onChange() {
+ this.dateAfterChange.emit(this.dateAfter)
+ this.dateBeforeChange.emit(this.dateBefore)
this.datesSet.emit({after: this.dateAfter, before: this.dateBefore})
}
@@ -91,12 +99,12 @@ export class DateDropdownComponent implements OnInit, OnDestroy {
}
clearBefore() {
- this.dateBefore = null;
+ this.dateBefore = null
this.onChange()
}
clearAfter() {
- this.dateAfter = null;
+ this.dateAfter = null
this.onChange()
}
diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html
index 5f9440336..1261ff921 100644
--- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html
+++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html
@@ -1,14 +1,14 @@
-
0 ? 'btn-primary' : 'btn-outline-primary'">
+ 0 ? 'btn-primary' : 'btn-outline-primary'">
{{title}}
- 0">
+ 0">
- {{itemsSelected?.length}}
+ {{selectionModel.selectionSize()}}
@@ -19,9 +19,9 @@
-
-
-
+
diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts
index 45a037f65..31c9d064e 100644
--- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts
+++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts
@@ -3,12 +3,55 @@ import { FilterPipe } from 'src/app/pipes/filter.pipe';
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
import { ToggleableItem, ToggleableItemState } from './toggleable-dropdown-button/toggleable-dropdown-button.component';
import { MatchingModel } from 'src/app/data/matching-model';
+import { Subject } from 'rxjs';
export enum FilterableDropdownType {
Filtering = 'filtering',
Editing = 'editing'
}
+export class FilterableDropdownSelectionModel {
+
+ changed = new Subject()
+
+ multiple = false
+
+ items: ToggleableItem[] = []
+
+ getSelected() {
+ return this.items.filter(i => i.state == ToggleableItemState.Selected).map(i => i.item)
+ }
+
+
+
+ toggle(item: MatchingModel, fireEvent = true) {
+ console.log("TOGGLE TAG")
+ let toggleableItem = this.items.find(i => i.item == item)
+ console.log(toggleableItem)
+
+ if (toggleableItem) {
+ if (toggleableItem.state == ToggleableItemState.Selected) {
+ toggleableItem.state = ToggleableItemState.NotSelected
+ } else {
+ this.items.forEach(i => {
+ if (i.item == item) {
+ i.state = ToggleableItemState.Selected
+ } else if (!this.multiple) {
+ i.state = ToggleableItemState.NotSelected
+ }
+ })
+ }
+ if (fireEvent) {
+ this.changed.next(this)
+ }
+ }
+ }
+
+ selectionSize() {
+ return this.getSelected().length
+ }
+}
+
@Component({
selector: 'app-filterable-dropdown',
templateUrl: './filterable-dropdown.component.html',
@@ -24,33 +67,45 @@ export class FilterableDropdownComponent {
@Input()
set items(items: MatchingModel[]) {
if (items) {
- this._toggleableItems = items.map(i => {
+ this._selectionModel.items = items.map(i => {
return {item: i, state: ToggleableItemState.NotSelected, count: i.document_count}
})
}
}
- _toggleableItems: ToggleableItem[] = []
-
- @Input()
- set toggleableItems (toggleableItems: ToggleableItem[]) {
- if (this.type == FilterableDropdownType.Editing && this.dropdown?.isOpen()) return
- else this._toggleableItems = toggleableItems
+ get items(): MatchingModel[] {
+ return this._selectionModel.items.map(i => i.item)
}
- get toggleableItems(): ToggleableItem[] {
- return this._toggleableItems
- }
+ _selectionModel = new FilterableDropdownSelectionModel()
@Input()
- set itemsSelected(itemsSelected: MatchingModel[]) {
- this.toggleableItems.forEach(i => {
- i.state = (itemsSelected.find(is => is.id == i.item.id)) ? ToggleableItemState.Selected : ToggleableItemState.NotSelected
+ 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 itemsSelected(): MatchingModel[] {
- return this.toggleableItems.filter(ti => ti.state == ToggleableItemState.Selected).map(ti => ti.item)
+ get selectionModel(): FilterableDropdownSelectionModel {
+ return this._selectionModel
+ }
+
+ @Output()
+ selectionModelChange = new EventEmitter()
+
+ @Input()
+ set multiple(value: boolean) {
+ this.selectionModel.multiple = value
+ }
+
+ get multiple() {
+ return this.selectionModel.multiple
}
@Input()
@@ -64,50 +119,40 @@ export class FilterableDropdownComponent {
types = FilterableDropdownType
- @Input()
- singular: boolean = false
-
- @Output()
- toggle = new EventEmitter()
-
- @Output()
- open = new EventEmitter()
-
- @Output()
- editingComplete = new EventEmitter()
-
hasBeenToggled:boolean = false
- constructor(private filterPipe: FilterPipe) { }
+ constructor(private filterPipe: FilterPipe) {
+ this.selectionModel = new FilterableDropdownSelectionModel()
+ }
toggleItem(toggleableItem: ToggleableItem): void {
- if (this.singular && toggleableItem.state == ToggleableItemState.Selected) {
- this._toggleableItems.filter(ti => ti.item.id !== toggleableItem.item.id).forEach(ti => ti.state = ToggleableItemState.NotSelected)
- }
- this.hasBeenToggled = true
- this.toggle.emit(toggleableItem.item)
+ // if (this.singular && toggleableItem.state == ToggleableItemState.Selected) {
+ // this.selectionModel.items.filter(ti => ti.item.id !== toggleableItem.item.id).forEach(ti => ti.state = ToggleableItemState.NotSelected)
+ // }
+ // this.hasBeenToggled = true
+ // this.toggle.emit(toggleableItem.item)
}
dropdownOpenChange(open: boolean): void {
- if (open) {
- setTimeout(() => {
- this.listFilterTextInput.nativeElement.focus();
- }, 0)
- this.hasBeenToggled = false
- this.open.next()
- } else {
- this.filterText = ''
- if (this.type == FilterableDropdownType.Editing) this.editingComplete.emit(this.toggleableItems)
- }
+ // if (open) {
+ // setTimeout(() => {
+ // this.listFilterTextInput.nativeElement.focus();
+ // }, 0)
+ // this.hasBeenToggled = false
+ // this.open.next()
+ // } else {
+ // this.filterText = ''
+ // if (this.type == FilterableDropdownType.Editing) this.editingComplete.emit(this.toggleableItems)
+ // }
}
listFilterEnter(): void {
- let filtered = this.filterPipe.transform(this.toggleableItems, this.filterText)
- if (filtered.length == 1) {
- let toggleableItem = this.toggleableItems.find(ti => ti.item.id == filtered[0].item.id)
- if (toggleableItem) toggleableItem.state = ToggleableItemState.Selected
- this.toggleItem(filtered[0])
- this.dropdown.close()
- }
+ // let filtered = this.filterPipe.transform(this.toggleableItems, this.filterText)
+ // if (filtered.length == 1) {
+ // let toggleableItem = this.toggleableItems.find(ti => ti.item.id == filtered[0].item.id)
+ // if (toggleableItem) toggleableItem.state = ToggleableItemState.Selected
+ // this.toggleItem(filtered[0])
+ // this.dropdown.close()
+ // }
}
}
diff --git a/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts b/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts
index cffbc916f..b881edd5d 100644
--- a/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts
+++ b/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts
@@ -31,8 +31,7 @@ export class ToggleableDropdownButtonComponent {
}
toggleItem(): void {
- this.toggleableItem.state = (this.toggleableItem.state == ToggleableItemState.NotSelected || this.toggleableItem.state == ToggleableItemState.PartiallySelected) ? ToggleableItemState.Selected : ToggleableItemState.NotSelected
- this.toggle.emit(this.toggleableItem)
+ this.toggle.emit()
}
getSelectedIconName() {
diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html
index fd619717f..800ef3742 100644
--- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html
+++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html
@@ -1,4 +1,4 @@
-
+
diff --git a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.html b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.html
index 2d480bfed..f029275eb 100644
--- a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.html
+++ b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.html
@@ -8,11 +8,11 @@
diff --git a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts
index dfc0e4e2c..3b072c434 100644
--- a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts
+++ b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts
@@ -3,14 +3,13 @@ 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 } from '@ng-bootstrap/ng-bootstrap';
+import { debounceTime, distinctUntilChanged, filter, flatMap, mergeMap } 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_TAG, FILTER_RULE_TYPES, FILTER_TITLE } from 'src/app/data/filter-rule-type';
-import { DateSelection } from 'src/app/components/common/date-dropdown/date-dropdown.component';
+import { FilterableDropdownSelectionModel } from '../../common/filterable-dropdown/filterable-dropdown.component';
@Component({
selector: 'app-filter-editor',
@@ -46,37 +45,91 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
) { }
tags: PaperlessTag[] = []
- correspondents: PaperlessCorrespondent[]
+ correspondents: PaperlessCorrespondent[] = []
documentTypes: PaperlessDocumentType[] = []
+ _titleFilter = ""
+
+ tagSelectionModel = new FilterableDropdownSelectionModel()
+ correspondentSelectionModel = new FilterableDropdownSelectionModel()
+ documentTypeSelectionModel = new FilterableDropdownSelectionModel()
+
+ dateCreatedBefore: string
+ dateCreatedAfter: string
+ dateAddedBefore: string
+ dateAddedAfter: string
+
@Input()
- filterRules: FilterRule[]
+ set filterRules (value: FilterRule[]) {
+ console.log("SET FILTER RULES")
+ 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
+ }
+ })
+
+ this.tagService.getCachedMany(value.filter(v => v.rule_type == FILTER_HAS_TAG).map(rule => +rule.value)).subscribe(tags => {
+ console.log(tags)
+ tags.forEach(tag => this.tagSelectionModel.toggle(tag, false))
+ })
+ }
@Output()
filterRulesChange = new EventEmitter
()
+ updateRules() {
+ console.log("UPDATE RULES!!!")
+ let filterRules: FilterRule[] = []
+ if (this._titleFilter) {
+ filterRules.push({rule_type: FILTER_TITLE, value: this._titleFilter})
+ }
+ this.tagSelectionModel.getSelected().forEach(tag => {
+ filterRules.push({rule_type: FILTER_HAS_TAG, value: tag.id.toString()})
+ })
+ this.correspondentSelectionModel.getSelected().forEach(correspondent => {
+ filterRules.push({rule_type: FILTER_CORRESPONDENT, value: correspondent.id.toString()})
+ })
+ this.documentTypeSelectionModel.getSelected().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})
+ }
+ console.log(filterRules)
+ this.filterRulesChange.next(filterRules)
+ }
+
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))
+ return this._titleFilter ||
+ this.dateCreatedAfter || this.dateAddedBefore || this.dateCreatedAfter || this.dateCreatedBefore ||
+ this.tagSelectionModel.selectionSize() || this.correspondentSelectionModel.selectionSize() || this.documentTypeSelectionModel.selectionSize()
}
get titleFilter() {
- let existingRule = this.filterRules.find(rule => rule.rule_type == FILTER_TITLE)
- return existingRule ? existingRule.value : ''
+ return this._titleFilter
}
set titleFilter(value) {
@@ -97,142 +150,27 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
debounceTime(400),
distinctUntilChanged()
).subscribe(title => {
- this.setTitleRule(title)
+ this._titleFilter = title
+ this.updateRules()
})
}
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()
+ this._titleFilter = ""
+ this.updateRules()
}
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)
- }
}
}
From 4fb5dce5e76abe4c1ec026e2ffa781af5d3fae35 Mon Sep 17 00:00:00 2001
From: jonaswinkler
Date: Mon, 28 Dec 2020 00:52:28 +0100
Subject: [PATCH 55/73] selection model
---
.../filterable-dropdown.component.html | 4 +-
.../filterable-dropdown.component.ts | 76 +++++++++++--------
.../toggleable-dropdown-button.component.html | 6 +-
.../toggleable-dropdown-button.component.ts | 14 +++-
.../filter-editor/filter-editor.component.ts | 40 ++++++----
5 files changed, 87 insertions(+), 53 deletions(-)
diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html
index 1261ff921..7c9a133ce 100644
--- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html
+++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html
@@ -20,8 +20,8 @@
diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts
index 31c9d064e..acb07e0d4 100644
--- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts
+++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts
@@ -4,6 +4,7 @@ import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
import { ToggleableItem, ToggleableItemState } from './toggleable-dropdown-button/toggleable-dropdown-button.component';
import { MatchingModel } from 'src/app/data/matching-model';
import { Subject } from 'rxjs';
+import { ThrowStmt } from '@angular/compiler';
export enum FilterableDropdownType {
Filtering = 'filtering',
@@ -16,39 +17,56 @@ export class FilterableDropdownSelectionModel {
multiple = false
- items: ToggleableItem[] = []
+ items: MatchingModel[] = []
- getSelected() {
- return this.items.filter(i => i.state == ToggleableItemState.Selected).map(i => i.item)
+ selection = new Map()
+
+ getSelectedItems() {
+ return this.items.filter(i => this.selection.get(i.id) == ToggleableItemState.Selected)
}
-
-
- toggle(item: MatchingModel, fireEvent = true) {
- console.log("TOGGLE TAG")
- let toggleableItem = this.items.find(i => i.item == item)
- console.log(toggleableItem)
-
- if (toggleableItem) {
- if (toggleableItem.state == ToggleableItemState.Selected) {
- toggleableItem.state = ToggleableItemState.NotSelected
- } else {
- this.items.forEach(i => {
- if (i.item == item) {
- i.state = ToggleableItemState.Selected
- } else if (!this.multiple) {
- i.state = ToggleableItemState.NotSelected
- }
- })
- }
- if (fireEvent) {
- this.changed.next(this)
- }
+ set(id: number, state: ToggleableItemState, fireEvent = true) {
+ this.selection.set(id, state)
+ if (fireEvent) {
+ this.changed.next(this)
}
}
+ toggle(id: number, fireEvent = true) {
+ let state = this.selection.get(id)
+ if (state == null || state != ToggleableItemState.Selected) {
+ this.selection.set(id, ToggleableItemState.Selected)
+ } else if (state == ToggleableItemState.Selected) {
+ this.selection.set(id, ToggleableItemState.NotSelected)
+ }
+
+ if (!this.multiple) {
+ for (let key of this.selection.keys()) {
+ if (key != id) {
+ this.selection.set(key, ToggleableItemState.NotSelected)
+ }
+ }
+ }
+
+ if (fireEvent) {
+ this.changed.next(this)
+ }
+
+ }
+
+ get(id: number) {
+ return this.selection.get(id) || ToggleableItemState.NotSelected
+ }
+
selectionSize() {
- return this.getSelected().length
+ return this.getSelectedItems().length
+ }
+
+ clear(fireEvent = true) {
+ this.selection.clear()
+ if (fireEvent) {
+ this.changed.next(this)
+ }
}
}
@@ -67,14 +85,12 @@ export class FilterableDropdownComponent {
@Input()
set items(items: MatchingModel[]) {
if (items) {
- this._selectionModel.items = items.map(i => {
- return {item: i, state: ToggleableItemState.NotSelected, count: i.document_count}
- })
+ this._selectionModel.items = items
}
}
get items(): MatchingModel[] {
- return this._selectionModel.items.map(i => i.item)
+ return this._selectionModel.items
}
_selectionModel = new FilterableDropdownSelectionModel()
diff --git a/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.html b/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.html
index daa72b41d..5b58025d1 100644
--- a/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.html
+++ b/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.html
@@ -5,8 +5,8 @@
-
-
{{toggleableItem?.item.name}}
+
+
{{item.name}}
-
{{toggleableItem?.count}}
+
{{item.document_count}}
diff --git a/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts b/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts
index b881edd5d..1d42b0946 100644
--- a/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts
+++ b/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts
@@ -21,13 +21,19 @@ export enum ToggleableItemState {
export class ToggleableDropdownButtonComponent {
@Input()
- toggleableItem: ToggleableItem
+ item: MatchingModel
+
+ @Input()
+ state: ToggleableItemState
+
+ @Input()
+ count: number
@Output()
toggle = new EventEmitter()
get isTag(): boolean {
- return 'is_inbox_tag' in this.toggleableItem?.item // ~ this.item instanceof PaperlessTag
+ return 'is_inbox_tag' in this.item
}
toggleItem(): void {
@@ -35,9 +41,9 @@ export class ToggleableDropdownButtonComponent {
}
getSelectedIconName() {
- if (this.toggleableItem?.state == ToggleableItemState.Selected) {
+ if (this.state == ToggleableItemState.Selected) {
return "check"
- } else if (this.toggleableItem?.state == ToggleableItemState.PartiallySelected) {
+ } else if (this.state == ToggleableItemState.PartiallySelected) {
return "dash"
} else {
return ""
diff --git a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts
index 3b072c434..f881dc1d9 100644
--- a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts
+++ b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts
@@ -3,13 +3,14 @@ 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, filter, flatMap, mergeMap } from 'rxjs/operators';
+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_TAG, FILTER_RULE_TYPES, FILTER_TITLE } from 'src/app/data/filter-rule-type';
+import { FILTER_ADDED_AFTER, FILTER_ADDED_BEFORE, FILTER_CORRESPONDENT, FILTER_CREATED_AFTER, FILTER_CREATED_BEFORE, FILTER_DOCUMENT_TYPE, 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',
@@ -61,7 +62,6 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
@Input()
set filterRules (value: FilterRule[]) {
- console.log("SET FILTER RULES")
value.forEach(rule => {
switch (rule.rule_type) {
case FILTER_TITLE:
@@ -79,31 +79,34 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
case FILTER_ADDED_BEFORE:
this.dateAddedBefore = rule.value
break
+ case FILTER_HAS_TAG:
+ this.tagSelectionModel.set(+rule.value, ToggleableItemState.Selected, false)
+ break
+ case FILTER_CORRESPONDENT:
+ this.correspondentSelectionModel.set(+rule.value, ToggleableItemState.Selected, false)
+ break
+ case FILTER_DOCUMENT_TYPE:
+ this.documentTypeSelectionModel.set(+rule.value, ToggleableItemState.Selected, false)
+ break
}
})
-
- this.tagService.getCachedMany(value.filter(v => v.rule_type == FILTER_HAS_TAG).map(rule => +rule.value)).subscribe(tags => {
- console.log(tags)
- tags.forEach(tag => this.tagSelectionModel.toggle(tag, false))
- })
}
@Output()
filterRulesChange = new EventEmitter
()
updateRules() {
- console.log("UPDATE RULES!!!")
let filterRules: FilterRule[] = []
if (this._titleFilter) {
filterRules.push({rule_type: FILTER_TITLE, value: this._titleFilter})
}
- this.tagSelectionModel.getSelected().forEach(tag => {
+ this.tagSelectionModel.getSelectedItems().forEach(tag => {
filterRules.push({rule_type: FILTER_HAS_TAG, value: tag.id.toString()})
})
- this.correspondentSelectionModel.getSelected().forEach(correspondent => {
+ this.correspondentSelectionModel.getSelectedItems().forEach(correspondent => {
filterRules.push({rule_type: FILTER_CORRESPONDENT, value: correspondent.id.toString()})
})
- this.documentTypeSelectionModel.getSelected().forEach(documentType => {
+ this.documentTypeSelectionModel.getSelectedItems().forEach(documentType => {
filterRules.push({rule_type: FILTER_DOCUMENT_TYPE, value: documentType.id.toString()})
})
if (this.dateCreatedBefore) {
@@ -118,13 +121,12 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
if (this.dateAddedAfter) {
filterRules.push({rule_type: FILTER_ADDED_AFTER, value: this.dateAddedAfter})
}
- console.log(filterRules)
this.filterRulesChange.next(filterRules)
}
hasFilters() {
return this._titleFilter ||
- this.dateCreatedAfter || this.dateAddedBefore || this.dateCreatedAfter || this.dateCreatedBefore ||
+ this.dateAddedAfter || this.dateAddedBefore || this.dateCreatedAfter || this.dateCreatedBefore ||
this.tagSelectionModel.selectionSize() || this.correspondentSelectionModel.selectionSize() || this.documentTypeSelectionModel.selectionSize()
}
@@ -161,16 +163,26 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
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)
}
}
From 08beaf81d56a8130a49657db70e2bdb6b5bdd2dc Mon Sep 17 00:00:00 2001
From: Jonas Winkler
Date: Mon, 28 Dec 2020 12:03:30 +0100
Subject: [PATCH 56/73] Update README.md
---
README.md | 1 -
1 file changed, 1 deletion(-)
diff --git a/README.md b/README.md
index 7f3808905..eea41ce05 100644
--- a/README.md
+++ b/README.md
@@ -122,7 +122,6 @@ Paperless has been around a while now, and people are starting to build stuff on
These projects also exist, but their status and compatibility with paperless-ng is unknown.
* [Paperless Desktop](https://github.com/thomasbrueggemann/paperless-desktop): A desktop UI for your Paperless installation. Runs on Mac, Linux, and Windows.
-* [ansible-role-paperless](https://github.com/ovv/ansible-role-paperless): An easy way to get Paperless running via Ansible.
* [paperless-cli](https://github.com/stgarf/paperless-cli): A golang command line binary to interact with a Paperless instance.
# Important Note
From e2bea3aee38366011c49ca1e8ea04ab6a51b2ca1 Mon Sep 17 00:00:00 2001
From: jonaswinkler
Date: Mon, 28 Dec 2020 12:29:34 +0100
Subject: [PATCH 57/73] add missing index task
---
src/documents/bulk_edit.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/documents/bulk_edit.py b/src/documents/bulk_edit.py
index beb9c50f7..c2dc438bc 100644
--- a/src/documents/bulk_edit.py
+++ b/src/documents/bulk_edit.py
@@ -106,6 +106,10 @@ def modify_tags(doc_ids, add_tags, remove_tags):
document_id=doc, tag_id=tag) for (doc,tag) in itertools.product(affected_docs, add_tags)
], ignore_conflicts=True)
+ async_task(
+ "documents.tasks.bulk_index_documents",
+ document_ids=affected_docs
+ )
async_task("documents.tasks.bulk_rename_files", document_ids=affected_docs)
return "OK"
From e228e18f04c11e92c0ef9beba1321aacbf31568f Mon Sep 17 00:00:00 2001
From: jonaswinkler
Date: Mon, 28 Dec 2020 12:29:53 +0100
Subject: [PATCH 58/73] it must have been late when I tried to do this
---
src/documents/serialisers.py | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)
diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py
index 155030a68..1f33b957c 100644
--- a/src/documents/serialisers.py
+++ b/src/documents/serialisers.py
@@ -236,6 +236,15 @@ class BulkEditSerializer(serializers.Serializer):
raise serializers.ValidationError(
f"Some documents in {name} don't exist or were specified twice.")
+ def _validate_tag_id_list(self, tags, name="tags"):
+ if not type(tags) == list:
+ raise serializers.ValidationError(f"{name} must be a list")
+ if not all([type(i) == int for i in tags]):
+ raise serializers.ValidationError(f"{name} must be a list of integers")
+ count = Tag.objects.filter(id__in=tags).count()
+ if not count == len(tags):
+ raise serializers.ValidationError(
+ f"Some tags in {name} don't exist or were specified twice.")
def validate_documents(self, documents):
self._validate_document_id_list(documents)
@@ -296,12 +305,12 @@ class BulkEditSerializer(serializers.Serializer):
def _validate_parameters_modify_tags(self, parameters):
if "add_tags" in parameters:
- self._validate_document_id_list(parameters['add_tags'], "add_tags")
+ self._validate_tag_id_list(parameters['add_tags'], "add_tags")
else:
raise serializers.ValidationError("add_tags not specified")
if "remove_tags" in parameters:
- self._validate_document_id_list(parameters['remove_tags'], "remove_tags")
+ self._validate_tag_id_list(parameters['remove_tags'], "remove_tags")
else:
raise serializers.ValidationError("remove_tags not specified")
From 9e311241b313844a50989c3b1aee9d01087374ba Mon Sep 17 00:00:00 2001
From: jonaswinkler
Date: Mon, 28 Dec 2020 12:31:14 +0100
Subject: [PATCH 59/73] rename some stuff
---
src-ui/src/app/services/rest/document.service.ts | 9 ++++-----
src/documents/views.py | 2 +-
2 files changed, 5 insertions(+), 6 deletions(-)
diff --git a/src-ui/src/app/services/rest/document.service.ts b/src-ui/src/app/services/rest/document.service.ts
index 9caaecdb2..c42510270 100644
--- a/src-ui/src/app/services/rest/document.service.ts
+++ b/src-ui/src/app/services/rest/document.service.ts
@@ -28,9 +28,9 @@ export interface SelectionDataItem {
}
export interface SelectionData {
- correspondents: SelectionDataItem[]
- tags: SelectionDataItem[]
- document_types: SelectionDataItem[]
+ selected_correspondents: SelectionDataItem[]
+ selected_tags: SelectionDataItem[]
+ selected_document_types: SelectionDataItem[]
}
@Injectable({
@@ -125,8 +125,7 @@ export class DocumentService extends AbstractPaperlessService
})
}
- selectionData(ids: number[]): Observable {
- console.log(ids)
+ getSelectionData(ids: number[]): Observable {
return this.http.post(this.getResourceUrl(null, 'selection_data'), {"documents": ids})
}
diff --git a/src/documents/views.py b/src/documents/views.py
index a50896057..ddf02f534 100755
--- a/src/documents/views.py
+++ b/src/documents/views.py
@@ -420,7 +420,7 @@ class SelectionDataView(APIView):
"id": t.id,
"document_count": t.document_count
} for t in tags],
- "selected_types": [{
+ "selected_document_types": [{
"id": t.id,
"document_count": t.document_count
} for t in types]
From bd02c789663221eb34ff3dacbe5d40b0405d5848 Mon Sep 17 00:00:00 2001
From: jonaswinkler
Date: Mon, 28 Dec 2020 12:31:30 +0100
Subject: [PATCH 60/73] fix the filter pipe
---
src-ui/src/app/pipes/filter.pipe.ts | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/src-ui/src/app/pipes/filter.pipe.ts b/src-ui/src/app/pipes/filter.pipe.ts
index a982e9019..8465f1533 100644
--- a/src-ui/src/app/pipes/filter.pipe.ts
+++ b/src-ui/src/app/pipes/filter.pipe.ts
@@ -1,17 +1,18 @@
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({
name: 'filter'
})
export class FilterPipe implements PipeTransform {
- transform(toggleableItems: ToggleableItem[], searchText: string): any[] {
- if (!toggleableItems) return [];
- if (!searchText) return toggleableItems;
+ transform(items: MatchingModel[], searchText: string): any[] {
+ if (!items) return [];
+ if (!searchText) return items;
- return toggleableItems.filter(toggleableItem => {
- return Object.keys(toggleableItem.item).some(key => {
- return String(toggleableItem.item[key]).toLowerCase().includes(searchText.toLowerCase());
+ return items.filter(item => {
+ return Object.keys(item).some(key => {
+ return String(item[key]).toLowerCase().includes(searchText.toLowerCase());
});
});
}
From 544ca8d0082325d23276c7164e3af07d0f67ccd7 Mon Sep 17 00:00:00 2001
From: jonaswinkler
Date: Mon, 28 Dec 2020 12:31:50 +0100
Subject: [PATCH 61/73] add ability to manually clear the cache on matching
models
---
.../app/services/rest/abstract-paperless-service.ts | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
diff --git a/src-ui/src/app/services/rest/abstract-paperless-service.ts b/src-ui/src/app/services/rest/abstract-paperless-service.ts
index f57956754..8ad1a2141 100644
--- a/src-ui/src/app/services/rest/abstract-paperless-service.ts
+++ b/src-ui/src/app/services/rest/abstract-paperless-service.ts
@@ -74,27 +74,31 @@ export abstract class AbstractPaperlessService {
)
}
+ clearCache() {
+ this._listAll = null
+ }
+
get(id: number): Observable {
return this.http.get(this.getResourceUrl(id))
}
create(o: T): Observable {
- this._listAll = null
+ this.clearCache()
return this.http.post(this.getResourceUrl(), o)
}
delete(o: T): Observable {
- this._listAll = null
+ this.clearCache()
return this.http.delete(this.getResourceUrl(o.id))
}
update(o: T): Observable {
- this._listAll = null
+ this.clearCache()
return this.http.put(this.getResourceUrl(o.id), o)
}
patch(o: T): Observable {
- this._listAll = null
+ this.clearCache()
return this.http.patch(this.getResourceUrl(o.id), o)
}
From 527c533958e14d5ab83b99c8d08b6a553cde1ec0 Mon Sep 17 00:00:00 2001
From: jonaswinkler
Date: Mon, 28 Dec 2020 12:32:21 +0100
Subject: [PATCH 62/73] improve performance of the toggle dropdown button
---
.../toggleable-dropdown-button.component.html | 14 +++++++++++---
.../toggleable-dropdown-button.component.ts | 15 +++++++--------
2 files changed, 18 insertions(+), 11 deletions(-)
diff --git a/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.html b/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.html
index 5b58025d1..404ea9d4d 100644
--- a/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.html
+++ b/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.html
@@ -1,8 +1,16 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts b/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts
index 1d42b0946..f082dfe67 100644
--- a/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts
+++ b/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts
@@ -40,13 +40,12 @@ export class ToggleableDropdownButtonComponent {
this.toggle.emit()
}
- getSelectedIconName() {
- if (this.state == ToggleableItemState.Selected) {
- return "check"
- } else if (this.state == ToggleableItemState.PartiallySelected) {
- return "dash"
- } else {
- return ""
- }
+ isChecked() {
+ return this.state == ToggleableItemState.Selected
}
+
+ isPartiallyChecked() {
+ return this.state == ToggleableItemState.PartiallySelected
+ }
+
}
From 67953c98a94dd0ee661d250867f7021967779ded Mon Sep 17 00:00:00 2001
From: jonaswinkler
Date: Mon, 28 Dec 2020 12:32:36 +0100
Subject: [PATCH 63/73] remove some non-required stuff
---
.../document-list/document-list.component.ts | 10 +---------
1 file changed, 1 insertion(+), 9 deletions(-)
diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts
index 60a483816..d83f02678 100644
--- a/src-ui/src/app/components/document-list/document-list.component.ts
+++ b/src-ui/src/app/components/document-list/document-list.component.ts
@@ -4,16 +4,11 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { PaperlessDocument } from 'src/app/data/paperless-document';
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
-import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
-import { DocumentTypeService } from 'src/app/services/rest/document-type.service';
import { 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 { Toast, ToastService } from 'src/app/services/toast.service';
import { FilterEditorComponent } from './filter-editor/filter-editor.component';
-import { SelectDialogComponent } from '../common/select-dialog/select-dialog.component';
import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component';
-import { ChangedItems } from './bulk-editor/bulk-editor.component';
@Component({
selector: 'app-document-list',
@@ -28,10 +23,7 @@ export class DocumentListComponent implements OnInit {
public route: ActivatedRoute,
private router: Router,
private toastService: ToastService,
- private modalService: NgbModal,
- private correspondentService: CorrespondentService,
- private documentTypeService: DocumentTypeService,
- private tagService: TagService) { }
+ private modalService: NgbModal) { }
@ViewChild("filterEditor")
private filterEditor: FilterEditorComponent
From 8af0259671e60862233f45cda651ea7a0bca19f1 Mon Sep 17 00:00:00 2001
From: jonaswinkler
Date: Mon, 28 Dec 2020 12:36:26 +0100
Subject: [PATCH 64/73] rework the bulk editor
---
.../filterable-dropdown.component.html | 8 +-
.../filterable-dropdown.component.ts | 109 ++++++---
.../bulk-editor/bulk-editor.component.html | 42 ++--
.../bulk-editor/bulk-editor.component.ts | 223 ++++++------------
4 files changed, 168 insertions(+), 214 deletions(-)
diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html
index 7c9a133ce..d7b2af6d3 100644
--- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html
+++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html
@@ -1,12 +1,12 @@
-
0 ? 'btn-primary' : 'btn-outline-primary'">
+ 0 ? 'btn-primary' : 'btn-outline-primary'">
{{title}}
- 0">
+ 0">
{{selectionModel.selectionSize()}}
@@ -24,8 +24,8 @@
-
- 0}">Apply
+
+ Apply
diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts
index acb07e0d4..8fb2d25d9 100644
--- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts
+++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts
@@ -1,14 +1,13 @@
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 { ToggleableItem, ToggleableItemState } from './toggleable-dropdown-button/toggleable-dropdown-button.component';
+import { ToggleableItemState } from './toggleable-dropdown-button/toggleable-dropdown-button.component';
import { MatchingModel } from 'src/app/data/matching-model';
import { Subject } from 'rxjs';
-import { ThrowStmt } from '@angular/compiler';
-export enum FilterableDropdownType {
- Filtering = 'filtering',
- Editing = 'editing'
+export interface ChangedItems {
+ itemsToAdd: MatchingModel[],
+ itemsToRemove: MatchingModel[]
}
export class FilterableDropdownSelectionModel {
@@ -19,31 +18,37 @@ export class FilterableDropdownSelectionModel {
items: MatchingModel[] = []
- selection = new Map()
+ private selectionStates = new Map()
+
+ private temporarySelectionStates = new Map()
getSelectedItems() {
- return this.items.filter(i => this.selection.get(i.id) == ToggleableItemState.Selected)
+ return this.items.filter(i => this.temporarySelectionStates.get(i.id) == ToggleableItemState.Selected)
}
set(id: number, state: ToggleableItemState, fireEvent = true) {
- this.selection.set(id, state)
+ 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.selection.get(id)
+ let state = this.temporarySelectionStates.get(id)
if (state == null || state != ToggleableItemState.Selected) {
- this.selection.set(id, ToggleableItemState.Selected)
+ this.temporarySelectionStates.set(id, ToggleableItemState.Selected)
} else if (state == ToggleableItemState.Selected) {
- this.selection.set(id, ToggleableItemState.NotSelected)
+ this.temporarySelectionStates.delete(id)
}
if (!this.multiple) {
- for (let key of this.selection.keys()) {
+ for (let key of this.temporarySelectionStates.keys()) {
if (key != id) {
- this.selection.set(key, ToggleableItemState.NotSelected)
+ this.temporarySelectionStates.delete(key)
}
}
}
@@ -55,7 +60,7 @@ export class FilterableDropdownSelectionModel {
}
get(id: number) {
- return this.selection.get(id) || ToggleableItemState.NotSelected
+ return this.temporarySelectionStates.get(id) || ToggleableItemState.NotSelected
}
selectionSize() {
@@ -63,11 +68,47 @@ export class FilterableDropdownSelectionModel {
}
clear(fireEvent = true) {
- this.selection.clear()
+ 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
+ }
+ }
+
+ 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({
@@ -131,35 +172,35 @@ export class FilterableDropdownComponent {
icon: string
@Input()
- type: FilterableDropdownType = FilterableDropdownType.Filtering
+ editing = false
- types = FilterableDropdownType
+ @Output()
+ apply = new EventEmitter()
- hasBeenToggled:boolean = false
+ @Output()
+ open = new EventEmitter()
constructor(private filterPipe: FilterPipe) {
this.selectionModel = new FilterableDropdownSelectionModel()
}
- toggleItem(toggleableItem: ToggleableItem): void {
- // if (this.singular && toggleableItem.state == ToggleableItemState.Selected) {
- // this.selectionModel.items.filter(ti => ti.item.id !== toggleableItem.item.id).forEach(ti => ti.state = ToggleableItemState.NotSelected)
- // }
- // this.hasBeenToggled = true
- // this.toggle.emit(toggleableItem.item)
+ applyClicked() {
+ if (this.selectionModel.isDirty()) {
+ this.dropdown.close()
+ this.apply.emit(this.selectionModel.diff())
+ }
}
dropdownOpenChange(open: boolean): void {
- // if (open) {
- // setTimeout(() => {
- // this.listFilterTextInput.nativeElement.focus();
- // }, 0)
- // this.hasBeenToggled = false
- // this.open.next()
- // } else {
- // this.filterText = ''
- // if (this.type == FilterableDropdownType.Editing) this.editingComplete.emit(this.toggleableItems)
- // }
+ if (open) {
+ setTimeout(() => {
+ this.listFilterTextInput.nativeElement.focus();
+ }, 0)
+ this.selectionModel.reset()
+ this.open.next()
+ } else {
+ this.filterText = ''
+ }
}
listFilterEnter(): void {
diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html
index 800ef3742..c78f3ea3e 100644
--- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html
+++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html
@@ -1,6 +1,6 @@
-
+
diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts
index 4365e00b0..36577bc6f 100644
--- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts
+++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts
@@ -1,26 +1,19 @@
-import { Component, EventEmitter, Input, Output } from '@angular/core';
+import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
-import { ObjectWithId } from 'src/app/data/object-with-id';
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 { PaperlessDocument } from 'src/app/data/paperless-document';
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 } from 'src/app/services/rest/document.service';
+import { DocumentService, SelectionDataItem } from 'src/app/services/rest/document.service';
import { OpenDocumentsService } from 'src/app/services/open-documents.service';
-import { FilterableDropdownType } from 'src/app/components/common/filterable-dropdown/filterable-dropdown.component';
import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component';
-import { ToggleableItem, ToggleableItemState } from 'src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component';
-
-export interface ChangedItems {
- itemsToAdd: any[],
- itemsToRemove: any[]
-}
+import { ChangedItems, FilterableDropdownSelectionModel } from '../../common/filterable-dropdown/filterable-dropdown.component';
+import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component';
@Component({
selector: 'app-bulk-editor',
@@ -33,69 +26,15 @@ export class BulkEditorComponent {
correspondents: PaperlessCorrespondent[]
documentTypes: PaperlessDocumentType[]
- private initialTagsToggleableItems: ToggleableItem[]
- private initialCorrespondentsToggleableItems: ToggleableItem[]
- private initialDocumentTypesToggleableItems: ToggleableItem[]
-
- dropdownTypes = FilterableDropdownType
-
- private _tagsToggleableItems: ToggleableItem[]
- get tagsToggleableItems(): ToggleableItem[] {
- let tagsToggleableItems = []
- let selectedDocuments: PaperlessDocument[] = this.documentList.documents.filter(d => this.documentList.selected.has(d.id))
-
- this.tags?.forEach(t => {
- let selectedDocumentsWithTag: PaperlessDocument[] = selectedDocuments.filter(d => d.tags.includes(t.id))
- let state = ToggleableItemState.NotSelected
- if (selectedDocuments.length > 0 && selectedDocumentsWithTag.length == selectedDocuments.length) state = ToggleableItemState.Selected
- else if (selectedDocumentsWithTag.length > 0 && selectedDocumentsWithTag.length < selectedDocuments.length) state = ToggleableItemState.PartiallySelected
- tagsToggleableItems.push({item: t, state: state, count: selectedDocumentsWithTag.length})
- })
- this._tagsToggleableItems = tagsToggleableItems
- return tagsToggleableItems
- }
-
- private _correspondentsToggleableItems: ToggleableItem[]
- get correspondentsToggleableItems(): ToggleableItem[] {
- let correspondentsToggleableItems = []
- let selectedDocuments: PaperlessDocument[] = this.documentList.documents.filter(d => this.documentList.selected.has(d.id))
-
- this.correspondents?.forEach(c => {
- let selectedDocumentsWithCorrespondent: PaperlessDocument[] = selectedDocuments.filter(d => d.correspondent == c.id)
- let state = ToggleableItemState.NotSelected
- if (selectedDocuments.length > 0 && selectedDocumentsWithCorrespondent.length == selectedDocuments.length) state = ToggleableItemState.Selected
- else if (selectedDocumentsWithCorrespondent.length > 0 && selectedDocumentsWithCorrespondent.length < selectedDocuments.length) state = ToggleableItemState.PartiallySelected
- correspondentsToggleableItems.push({item: c, state: state, count: selectedDocumentsWithCorrespondent.length})
- })
- this._correspondentsToggleableItems = correspondentsToggleableItems
- return correspondentsToggleableItems
- }
-
- private _documentTypesToggleableItems: ToggleableItem[]
- get documentTypesToggleableItems(): ToggleableItem[] {
- let documentTypesToggleableItems = []
- let selectedDocuments: PaperlessDocument[] = this.documentList.documents.filter(d => this.documentList.selected.has(d.id))
-
- this.documentTypes?.forEach(dt => {
- let selectedDocumentsWithDocumentType: PaperlessDocument[] = selectedDocuments.filter(d => d.document_type == dt.id)
- let state = ToggleableItemState.NotSelected
- if (selectedDocuments.length > 0 && selectedDocumentsWithDocumentType.length == selectedDocuments.length) state = ToggleableItemState.Selected
- else if (selectedDocumentsWithDocumentType.length > 0 && selectedDocumentsWithDocumentType.length < selectedDocuments.length) state = ToggleableItemState.PartiallySelected
- documentTypesToggleableItems.push({item: dt, state: state, count: selectedDocumentsWithDocumentType.length})
- })
- this._documentTypesToggleableItems = documentTypesToggleableItems
- return documentTypesToggleableItems
- }
-
- get documentList(): DocumentListViewService {
- return this.documentListViewService
- }
+ tagSelectionModel = new FilterableDropdownSelectionModel()
+ correspondentSelectionModel = new FilterableDropdownSelectionModel()
+ documentTypeSelectionModel = new FilterableDropdownSelectionModel()
constructor(
private documentTypeService: DocumentTypeService,
private tagService: TagService,
private correspondentService: CorrespondentService,
- private documentListViewService: DocumentListViewService,
+ public list: DocumentListViewService,
private documentService: DocumentService,
private modalService: NgbModal,
private openDocumentService: OpenDocumentsService
@@ -107,97 +46,69 @@ export class BulkEditorComponent {
this.documentTypeService.listAll().subscribe(result => this.documentTypes = result.results)
}
- tagsDropdownOpen() {
- this.initialTagsToggleableItems = this._tagsToggleableItems
- }
-
- correspondentsDropdownOpen() {
- this.initialCorrespondentsToggleableItems = this._correspondentsToggleableItems
- }
-
- documentTypesDropdownOpen() {
- this.initialDocumentTypesToggleableItems = this._documentTypesToggleableItems
- }
-
- private checkForChangedItems(toggleableItemsA: ToggleableItem[], toggleableItemsB: ToggleableItem[]): ChangedItems {
- let itemsToAdd: any[] = []
- let itemsToRemove: any[] = []
- toggleableItemsA.forEach(oldItem => {
- let newItem = toggleableItemsB.find(nTTI => nTTI.item.id == oldItem.item.id)
-
- if (newItem.state == ToggleableItemState.Selected && (oldItem.state == ToggleableItemState.PartiallySelected || oldItem.state == ToggleableItemState.NotSelected)) itemsToAdd.push(newItem.item)
- else if (newItem.state == ToggleableItemState.NotSelected && (oldItem.state == ToggleableItemState.Selected || oldItem.state == ToggleableItemState.PartiallySelected)) itemsToRemove.push(newItem.item)
- })
- return { itemsToAdd: itemsToAdd, itemsToRemove: itemsToRemove }
- }
-
private executeBulkOperation(method: string, args): Observable {
- return this.documentService.bulkEdit(Array.from(this.documentList.selected), method, args).pipe(
+ return this.documentService.bulkEdit(Array.from(this.list.selected), method, args).pipe(
tap(() => {
- this.documentList.reload()
- this.documentList.selected.forEach(id => {
+ this.list.reload()
+ this.list.selected.forEach(id => {
this.openDocumentService.refreshDocument(id)
})
- this.documentList.selectNone()
+ this.list.selectNone()
})
)
}
- setTags(newTagsToggleableItems: ToggleableItem[]) {
- let changedTags: ChangedItems
- if (newTagsToggleableItems) {
- changedTags = this.checkForChangedItems(this.initialTagsToggleableItems, newTagsToggleableItems)
- if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length == 0) return
- }
+ private applySelectionData(items: SelectionDataItem[], selectionModel: FilterableDropdownSelectionModel) {
+ let selectionData = new Map()
+ 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)
+ }
- let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
- modal.componentInstance.title = "Confirm Tags Assignment"
- let action = 'set_tags'
- let tags
- let messageFragment = ''
- let both = changedTags && changedTags.itemsToAdd.length > 0 && changedTags.itemsToRemove.length > 0
- if (!changedTags) {
- messageFragment = `remove all tags from`
- } else {
- if (changedTags.itemsToAdd.length > 0) {
- tags = changedTags.itemsToAdd
- messageFragment = `assign the tag(s) ${changedTags.itemsToAdd.map(t => t.name).join(', ')} to`
- }
- if (changedTags.itemsToRemove.length > 0) {
- if (!both) {
- action = 'remove_tags'
- tags = changedTags.itemsToRemove
- } else {
- messageFragment += ' and '
- }
- messageFragment += `remove the tag(s) ${changedTags.itemsToRemove.map(t => t.name).join(', ')} from`
- }
- }
- modal.componentInstance.message = `This operation will ${messageFragment} all ${this.documentList.selected.size} selected document(s).`
- modal.componentInstance.btnClass = "btn-warning"
- modal.componentInstance.btnCaption = "Confirm"
- modal.componentInstance.confirmClicked.subscribe(() => {
- // TODO: API endpoints for add/remove multiple tags
- this.executeBulkOperation(action, {"tags": tags ? tags.map(t => t.id) : null}).subscribe(
- response => {
- if (!both) modal.close()
- else {
- this.executeBulkOperation('remove_tags', {"tags": changedTags.itemsToRemove.map(t => t.id)}).subscribe(
- response => {
- modal.close()
- })
- }
- }
- )
+ openTagsDropdown() {
+ this.documentService.getSelectionData(Array.from(this.list.selected)).subscribe(s => {
+ this.applySelectionData(s.selected_tags, this.tagSelectionModel)
})
}
- setCorrespondents(newCorrespondentsToggleableItems: ToggleableItem[]) {
- let changedCorrespondents: ChangedItems
- if (newCorrespondentsToggleableItems) {
- changedCorrespondents = this.checkForChangedItems(this.initialCorrespondentsToggleableItems, newCorrespondentsToggleableItems)
- if (changedCorrespondents.itemsToAdd.length == 0 && changedCorrespondents.itemsToRemove.length == 0) return
- }
+ 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)
+ })
+ }
+
+ setTags(changedTags: ChangedItems) {
+ if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length == 0) return
+
+ let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
+ modal.componentInstance.title = "Confirm Tags Assignment"
+
+ modal.componentInstance.message = `This operation will modify some tags on all ${this.list.selected.size} selected document(s).`
+ modal.componentInstance.btnClass = "btn-warning"
+ modal.componentInstance.btnCaption = "Confirm"
+ modal.componentInstance.confirmClicked.subscribe(() => {
+ this.executeBulkOperation('modify_tags', {"add_tags": changedTags.itemsToAdd.map(t => t.id), "remove_tags": changedTags.itemsToRemove.map(t => t.id)}).subscribe(
+ response => {
+ this.tagService.clearCache()
+ modal.close()
+ })
+ }
+ )
+ }
+
+ setCorrespondents(changedCorrespondents: ChangedItems) {
+ if (changedCorrespondents.itemsToAdd.length == 0 && changedCorrespondents.itemsToRemove.length == 0) return
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
modal.componentInstance.title = "Confirm Correspondent Assignment"
@@ -207,24 +118,21 @@ export class BulkEditorComponent {
correspondent = changedCorrespondents.itemsToAdd[0]
messageFragment = `assign the correspondent ${correspondent.name} to`
}
- modal.componentInstance.message = `This operation will ${messageFragment} all ${this.documentList.selected.size} selected document(s).`
+ modal.componentInstance.message = `This operation will ${messageFragment} all ${this.list.selected.size} selected document(s).`
modal.componentInstance.btnClass = "btn-warning"
modal.componentInstance.btnCaption = "Confirm"
modal.componentInstance.confirmClicked.subscribe(() => {
this.executeBulkOperation('set_correspondent', {"correspondent": correspondent ? correspondent.id : null}).subscribe(
response => {
+ this.correspondentService.clearCache()
modal.close()
}
)
})
}
- setDocumentTypes(newDocumentTypesToggleableItems: ToggleableItem[]) {
- let changedDocumentTypes: ChangedItems
- if (newDocumentTypesToggleableItems) {
- changedDocumentTypes = this.checkForChangedItems(this.initialDocumentTypesToggleableItems, newDocumentTypesToggleableItems)
- if (changedDocumentTypes.itemsToAdd.length == 0 && changedDocumentTypes.itemsToRemove.length == 0) return
- }
+ setDocumentTypes(changedDocumentTypes: ChangedItems) {
+ if (changedDocumentTypes.itemsToAdd.length == 0 && changedDocumentTypes.itemsToRemove.length == 0) return
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
modal.componentInstance.title = "Confirm Document Type Assignment"
@@ -234,12 +142,13 @@ export class BulkEditorComponent {
documentType = changedDocumentTypes.itemsToAdd[0]
messageFragment = `assign the document type ${documentType.name} to`
}
- modal.componentInstance.message = `This operation will ${messageFragment} all ${this.documentList.selected.size} selected document(s).`
+ modal.componentInstance.message = `This operation will ${messageFragment} all ${this.list.selected.size} selected document(s).`
modal.componentInstance.btnClass = "btn-warning"
modal.componentInstance.btnCaption = "Confirm"
modal.componentInstance.confirmClicked.subscribe(() => {
this.executeBulkOperation('set_document_type', {"document_type": documentType ? documentType.id : null}).subscribe(
response => {
+ this.documentService.clearCache()
modal.close()
}
)
@@ -250,7 +159,7 @@ export class BulkEditorComponent {
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.documentList.selected.size} selected document(s).`
+ 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)"
From 7beb8a09299696e1e83cf6241f249f3b67943833 Mon Sep 17 00:00:00 2001
From: jonaswinkler
Date: Mon, 28 Dec 2020 12:48:41 +0100
Subject: [PATCH 65/73] fix enter select
---
.../filterable-dropdown.component.ts | 20 +++++++++++--------
src-ui/src/app/pipes/filter.pipe.ts | 2 +-
2 files changed, 13 insertions(+), 9 deletions(-)
diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts
index 8fb2d25d9..e06f0d638 100644
--- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts
+++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts
@@ -196,7 +196,9 @@ export class FilterableDropdownComponent {
setTimeout(() => {
this.listFilterTextInput.nativeElement.focus();
}, 0)
- this.selectionModel.reset()
+ if (this.editing) {
+ this.selectionModel.reset()
+ }
this.open.next()
} else {
this.filterText = ''
@@ -204,12 +206,14 @@ export class FilterableDropdownComponent {
}
listFilterEnter(): void {
- // let filtered = this.filterPipe.transform(this.toggleableItems, this.filterText)
- // if (filtered.length == 1) {
- // let toggleableItem = this.toggleableItems.find(ti => ti.item.id == filtered[0].item.id)
- // if (toggleableItem) toggleableItem.state = ToggleableItemState.Selected
- // this.toggleItem(filtered[0])
- // this.dropdown.close()
- // }
+ 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()
+ }
+ }
}
}
diff --git a/src-ui/src/app/pipes/filter.pipe.ts b/src-ui/src/app/pipes/filter.pipe.ts
index 8465f1533..d83ccc07a 100644
--- a/src-ui/src/app/pipes/filter.pipe.ts
+++ b/src-ui/src/app/pipes/filter.pipe.ts
@@ -6,7 +6,7 @@ import { MatchingModel } from '../data/matching-model';
name: 'filter'
})
export class FilterPipe implements PipeTransform {
- transform(items: MatchingModel[], searchText: string): any[] {
+ transform(items: MatchingModel[], searchText: string): MatchingModel[] {
if (!items) return [];
if (!searchText) return items;
From 3d173a13ab8706a57ae0985461857c556a6992f4 Mon Sep 17 00:00:00 2001
From: jonaswinkler
Date: Mon, 28 Dec 2020 13:31:22 +0100
Subject: [PATCH 66/73] move the two post bulk edit tasks into one
---
src/documents/bulk_edit.py | 29 ++++-------------------
src/documents/tasks.py | 9 ++------
src/documents/tests/test_api.py | 41 ++++++++++++++++++---------------
3 files changed, 30 insertions(+), 49 deletions(-)
diff --git a/src/documents/bulk_edit.py b/src/documents/bulk_edit.py
index c2dc438bc..18168a164 100644
--- a/src/documents/bulk_edit.py
+++ b/src/documents/bulk_edit.py
@@ -18,11 +18,7 @@ def set_correspondent(doc_ids, correspondent):
qs.update(correspondent=correspondent)
async_task(
- "documents.tasks.bulk_index_documents",
- document_ids=affected_docs
- )
-
- async_task("documents.tasks.bulk_rename_files", document_ids=affected_docs)
+ "documents.tasks.bulk_update_documents", document_ids=affected_docs)
return "OK"
@@ -37,11 +33,7 @@ def set_document_type(doc_ids, document_type):
qs.update(document_type=document_type)
async_task(
- "documents.tasks.bulk_index_documents",
- document_ids=affected_docs
- )
-
- async_task("documents.tasks.bulk_rename_files", document_ids=affected_docs)
+ "documents.tasks.bulk_update_documents", document_ids=affected_docs)
return "OK"
@@ -59,11 +51,7 @@ def add_tag(doc_ids, tag):
])
async_task(
- "documents.tasks.bulk_index_documents",
- document_ids=affected_docs
- )
-
- async_task("documents.tasks.bulk_rename_files", document_ids=affected_docs)
+ "documents.tasks.bulk_update_documents", document_ids=affected_docs)
return "OK"
@@ -81,11 +69,7 @@ def remove_tag(doc_ids, tag):
).delete()
async_task(
- "documents.tasks.bulk_index_documents",
- document_ids=affected_docs
- )
-
- async_task("documents.tasks.bulk_rename_files", document_ids=affected_docs)
+ "documents.tasks.bulk_update_documents", document_ids=affected_docs)
return "OK"
@@ -107,10 +91,7 @@ def modify_tags(doc_ids, add_tags, remove_tags):
], ignore_conflicts=True)
async_task(
- "documents.tasks.bulk_index_documents",
- document_ids=affected_docs
- )
- async_task("documents.tasks.bulk_rename_files", document_ids=affected_docs)
+ "documents.tasks.bulk_update_documents", document_ids=affected_docs)
return "OK"
diff --git a/src/documents/tasks.py b/src/documents/tasks.py
index c1f3ffbaa..f9937c177 100644
--- a/src/documents/tasks.py
+++ b/src/documents/tasks.py
@@ -90,16 +90,11 @@ def sanity_check():
return "No issues detected."
-def bulk_rename_files(document_ids):
- qs = Document.objects.filter(id__in=document_ids)
- for doc in qs:
- post_save.send(Document, instance=doc, created=False)
-
-
-def bulk_index_documents(document_ids):
+def bulk_update_documents(document_ids):
documents = Document.objects.filter(id__in=document_ids)
ix = index.open_index()
with AsyncWriter(ix) as writer:
for doc in documents:
index.update_document(writer, doc)
+ post_save.send(Document, instance=doc, created=False)
diff --git a/src/documents/tests/test_api.py b/src/documents/tests/test_api.py
index a9c855ff7..adc947a1a 100644
--- a/src/documents/tests/test_api.py
+++ b/src/documents/tests/test_api.py
@@ -699,49 +699,49 @@ class TestBulkEdit(DirectoriesMixin, APITestCase):
self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 1)
bulk_edit.set_correspondent([self.doc1.id, self.doc2.id, self.doc3.id], self.c2.id)
self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 3)
- self.assertEqual(self.async_task.call_count, 2)
- self.assertCountEqual(self.async_task.call_args_list[0][1]['document_ids'], [self.doc1.id, self.doc2.id])
- self.assertCountEqual(self.async_task.call_args_list[0][1]['document_ids'], [self.doc1.id, self.doc2.id])
+ self.async_task.assert_called_once()
+ args, kwargs = self.async_task.call_args
+ self.assertCountEqual(kwargs['document_ids'], [self.doc1.id, self.doc2.id])
def test_unset_correspondent(self):
self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 1)
bulk_edit.set_correspondent([self.doc1.id, self.doc2.id, self.doc3.id], None)
self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 0)
- self.assertEqual(self.async_task.call_count, 2)
- self.assertCountEqual(self.async_task.call_args_list[0][1]['document_ids'], [self.doc2.id, self.doc3.id])
- self.assertCountEqual(self.async_task.call_args_list[0][1]['document_ids'], [self.doc2.id, self.doc3.id])
+ self.async_task.assert_called_once()
+ args, kwargs = self.async_task.call_args
+ self.assertCountEqual(kwargs['document_ids'], [self.doc2.id, self.doc3.id])
def test_set_document_type(self):
self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 1)
bulk_edit.set_document_type([self.doc1.id, self.doc2.id, self.doc3.id], self.dt2.id)
self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 3)
- self.assertEqual(self.async_task.call_count, 2)
- self.assertCountEqual(self.async_task.call_args_list[0][1]['document_ids'], [self.doc1.id, self.doc2.id])
- self.assertCountEqual(self.async_task.call_args_list[0][1]['document_ids'], [self.doc1.id, self.doc2.id])
+ self.async_task.assert_called_once()
+ args, kwargs = self.async_task.call_args
+ self.assertCountEqual(kwargs['document_ids'], [self.doc1.id, self.doc2.id])
def test_unset_document_type(self):
self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 1)
bulk_edit.set_document_type([self.doc1.id, self.doc2.id, self.doc3.id], None)
self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 0)
- self.assertEqual(self.async_task.call_count, 2)
- self.assertCountEqual(self.async_task.call_args_list[0][1]['document_ids'], [self.doc2.id, self.doc3.id])
- self.assertCountEqual(self.async_task.call_args_list[0][1]['document_ids'], [self.doc2.id, self.doc3.id])
+ self.async_task.assert_called_once()
+ args, kwargs = self.async_task.call_args
+ self.assertCountEqual(kwargs['document_ids'], [self.doc2.id, self.doc3.id])
def test_add_tag(self):
self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 2)
bulk_edit.add_tag([self.doc1.id, self.doc2.id, self.doc3.id, self.doc4.id], self.t1.id)
self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 4)
- self.assertEqual(self.async_task.call_count, 2)
- self.assertCountEqual(self.async_task.call_args_list[0][1]['document_ids'], [self.doc1.id, self.doc3.id])
- self.assertCountEqual(self.async_task.call_args_list[0][1]['document_ids'], [self.doc1.id, self.doc3.id])
+ self.async_task.assert_called_once()
+ args, kwargs = self.async_task.call_args
+ self.assertCountEqual(kwargs['document_ids'], [self.doc1.id, self.doc3.id])
def test_remove_tag(self):
self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 2)
bulk_edit.remove_tag([self.doc1.id, self.doc3.id, self.doc4.id], self.t1.id)
self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 1)
- self.assertEqual(self.async_task.call_count, 2)
- self.assertCountEqual(self.async_task.call_args_list[0][1]['document_ids'], [self.doc4.id])
- self.assertCountEqual(self.async_task.call_args_list[0][1]['document_ids'], [self.doc4.id])
+ self.async_task.assert_called_once()
+ args, kwargs = self.async_task.call_args
+ self.assertCountEqual(kwargs['document_ids'], [self.doc4.id])
def test_modify_tags(self):
tag_unrelated = Tag.objects.create(name="unrelated")
@@ -752,6 +752,11 @@ class TestBulkEdit(DirectoriesMixin, APITestCase):
self.assertCountEqual(list(self.doc2.tags.all()), [self.t2, tag_unrelated])
self.assertCountEqual(list(self.doc3.tags.all()), [self.t2, tag_unrelated])
+ self.async_task.assert_called_once()
+ args, kwargs = self.async_task.call_args
+ # TODO: doc3 should not be affected, but the query for that is rather complicated
+ self.assertCountEqual(kwargs['document_ids'], [self.doc2.id, self.doc3.id])
+
def test_delete(self):
self.assertEqual(Document.objects.count(), 5)
bulk_edit.delete([self.doc1.id, self.doc2.id])
From aa6e96e54d6a29ef5a8ea5478457cc63515ec456 Mon Sep 17 00:00:00 2001
From: jonaswinkler
Date: Mon, 28 Dec 2020 13:33:58 +0100
Subject: [PATCH 67/73] fix pycodestyle
---
src/documents/bulk_edit.py | 6 +++---
src/documents/serialisers.py | 12 ++++++++----
2 files changed, 11 insertions(+), 7 deletions(-)
diff --git a/src/documents/bulk_edit.py b/src/documents/bulk_edit.py
index 18168a164..c0c80a795 100644
--- a/src/documents/bulk_edit.py
+++ b/src/documents/bulk_edit.py
@@ -85,9 +85,9 @@ def modify_tags(doc_ids, add_tags, remove_tags):
tag_id__in=remove_tags,
).delete()
- DocumentTagRelationship.objects.bulk_create([
- DocumentTagRelationship(
- document_id=doc, tag_id=tag) for (doc,tag) in itertools.product(affected_docs, add_tags)
+ DocumentTagRelationship.objects.bulk_create([DocumentTagRelationship(
+ document_id=doc, tag_id=tag) for (doc, tag) in itertools.product(
+ affected_docs, add_tags)
], ignore_conflicts=True)
async_task(
diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py
index 1f33b957c..66f5f883f 100644
--- a/src/documents/serialisers.py
+++ b/src/documents/serialisers.py
@@ -230,17 +230,20 @@ class BulkEditSerializer(serializers.Serializer):
if not type(documents) == list:
raise serializers.ValidationError(f"{name} must be a list")
if not all([type(i) == int for i in documents]):
- raise serializers.ValidationError(f"{name} must be a list of integers")
+ raise serializers.ValidationError(
+ f"{name} must be a list of integers")
count = Document.objects.filter(id__in=documents).count()
if not count == len(documents):
raise serializers.ValidationError(
- f"Some documents in {name} don't exist or were specified twice.")
+ f"Some documents in {name} don't exist or were "
+ f"specified twice.")
def _validate_tag_id_list(self, tags, name="tags"):
if not type(tags) == list:
raise serializers.ValidationError(f"{name} must be a list")
if not all([type(i) == int for i in tags]):
- raise serializers.ValidationError(f"{name} must be a list of integers")
+ raise serializers.ValidationError(
+ f"{name} must be a list of integers")
count = Tag.objects.filter(id__in=tags).count()
if not count == len(tags):
raise serializers.ValidationError(
@@ -310,7 +313,8 @@ class BulkEditSerializer(serializers.Serializer):
raise serializers.ValidationError("add_tags not specified")
if "remove_tags" in parameters:
- self._validate_tag_id_list(parameters['remove_tags'], "remove_tags")
+ self._validate_tag_id_list(parameters['remove_tags'],
+ "remove_tags")
else:
raise serializers.ValidationError("remove_tags not specified")
From d6e733c56f8724b95189011fd383829d830eb2df Mon Sep 17 00:00:00 2001
From: jonaswinkler
Date: Mon, 28 Dec 2020 15:39:53 +0100
Subject: [PATCH 68/73] add more localization tags #123
---
.../confirm-dialog/confirm-dialog.component.ts | 4 ++--
.../date-dropdown/date-dropdown.component.html | 8 ++++----
.../filterable-dropdown.component.html | 2 +-
.../select-dialog/select-dialog.component.html | 4 ++--
.../select-dialog/select-dialog.component.ts | 4 ++--
.../metadata-collapse.component.ts | 2 +-
.../bulk-editor/bulk-editor.component.html | 16 ++++++----------
.../document-list/document-list.component.html | 10 +++++-----
.../filter-editor/filter-editor.component.html | 18 +++++++++---------
.../filter-editor/filter-editor.component.ts | 6 +++---
10 files changed, 35 insertions(+), 39 deletions(-)
diff --git a/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.ts b/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.ts
index 4791d0e77..c397811a4 100644
--- a/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.ts
+++ b/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.ts
@@ -14,7 +14,7 @@ export class ConfirmDialogComponent implements OnInit {
public confirmClicked = new EventEmitter()
@Input()
- title = "Confirmation"
+ title = $localize`Confirmation`
@Input()
messageBold
@@ -26,7 +26,7 @@ export class ConfirmDialogComponent implements OnInit {
btnClass = "btn-primary"
@Input()
- btnCaption = "Confirm"
+ btnCaption = $localize`Confirm`
confirmButtonEnabled = true
seconds = 0
diff --git a/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.html b/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.html
index ba9f1fafb..e4f17c4e6 100644
--- a/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.html
+++ b/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.html
@@ -10,12 +10,12 @@
-
After
+
After
- Clear
+ Clear
@@ -26,12 +26,12 @@
-
Before
+
Before
- Clear
+ Clear
diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html
index d7b2af6d3..48e0dfd97 100644
--- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html
+++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html
@@ -25,7 +25,7 @@
- Apply
+ Apply
diff --git a/src-ui/src/app/components/common/select-dialog/select-dialog.component.html b/src-ui/src/app/components/common/select-dialog/select-dialog.component.html
index 8bde38d61..2184013a3 100644
--- a/src-ui/src/app/components/common/select-dialog/select-dialog.component.html
+++ b/src-ui/src/app/components/common/select-dialog/select-dialog.component.html
@@ -10,6 +10,6 @@
\ No newline at end of file
diff --git a/src-ui/src/app/components/common/select-dialog/select-dialog.component.ts b/src-ui/src/app/components/common/select-dialog/select-dialog.component.ts
index 76b23491c..99bf9b91a 100644
--- a/src-ui/src/app/components/common/select-dialog/select-dialog.component.ts
+++ b/src-ui/src/app/components/common/select-dialog/select-dialog.component.ts
@@ -15,10 +15,10 @@ export class SelectDialogComponent implements OnInit {
public selectClicked = new EventEmitter()
@Input()
- title = "Select"
+ title = $localize`Select`
@Input()
- message = "Please select an object"
+ message = $localize`Please select an object`
@Input()
objects: ObjectWithId[] = []
diff --git a/src-ui/src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts b/src-ui/src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts
index 160274e41..34bbbd655 100644
--- a/src-ui/src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts
+++ b/src-ui/src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts
@@ -15,7 +15,7 @@ export class MetadataCollapseComponent implements OnInit {
metadata
@Input()
- title = "Metadata"
+ title = $localize`Metadata`
ngOnInit(): void {
}
diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html
index c78f3ea3e..e08d50c47 100644
--- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html
+++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html
@@ -3,32 +3,29 @@
-
- Cancel
+ Cancel
diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html
index b5539a1e5..40a08c105 100644
--- a/src-ui/src/app/components/document-list/document-list.component.html
+++ b/src-ui/src/app/components/document-list/document-list.component.html
@@ -4,13 +4,13 @@