Compare commits

...

432 Commits

Author SHA1 Message Date
jonaswinkler
9c8b43f602 Merge branch 'dev' 2020-12-31 02:27:45 +01:00
jonaswinkler
70f052cb5c changelog 2020-12-31 02:15:16 +01:00
jonaswinkler
e5c10fcd93 Merge branch 'master' into dev 2020-12-31 02:15:01 +01:00
jonaswinkler
9eb377703e fix hover for check boxes 2020-12-31 01:58:32 +01:00
jonaswinkler
4c63dad309 fix sorting for "not assigned" 2020-12-31 01:43:04 +01:00
jonaswinkler
45e52aa985 add the manifest to the frontend entry page #219 2020-12-31 01:20:38 +01:00
jonaswinkler
6066d00c5e intelligent sorting of the bulk edit drop downs 2020-12-31 01:07:28 +01:00
jonaswinkler
59a10a8127 Merge branch 'dev' of github.com:jonaswinkler/paperless-ng into dev 2020-12-31 01:07:07 +01:00
Jonas Winkler
6dc8edea88 Merge pull request #219 from zjean/feature/add-to-home
Improve mobile add to homes screen support
2020-12-31 01:02:05 +01:00
jonaswinkler
ef1ee8e0c0 fixes a bug with invisible selection 2020-12-30 23:34:50 +01:00
jonaswinkler
1428b26703 this immensely improves resposibility 2020-12-30 23:34:21 +01:00
jonaswinkler
63b841c496 fixes #224 2020-12-30 23:01:34 +01:00
jonaswinkler
208f4291d6 fixes #222 2020-12-30 21:54:36 +01:00
jonaswinkler
a0f983f05d codestyle 2020-12-30 17:20:03 +01:00
jonaswinkler
1df922ef16 Do file renaming first, since this is the important step, and indexing takes a while. 2020-12-30 17:18:27 +01:00
jonaswinkler
beed820602 improve file renaming speed. 2020-12-30 17:17:36 +01:00
jonaswinkler
b6722fdd84 update angular messages 2020-12-30 17:14:08 +01:00
jonaswinkler
aacf7ba275 language fixes 2020-12-30 16:29:45 +01:00
jonaswinkler
400392655e adds a command to regenerate thumbnails #218 2020-12-30 15:46:56 +01:00
jonaswinkler
713985f259 fixes #218 2020-12-30 15:12:16 +01:00
Jan Wiebe
664280ccce Add newline to end of file 2020-12-30 14:56:18 +01:00
Jan Wiebe
9fe6c5c3a9 Fix indentation 2020-12-30 14:55:49 +01:00
Jan Wiebe
ab5098a036 Fix indentation of angular.json 2020-12-30 14:52:20 +01:00
Jan Wiebe
b092bb6848 Add manifest file to add to home screen and launch in full screen mode. 2020-12-30 14:45:15 +01:00
jonaswinkler
17ded12375 bugfix 2020-12-30 12:21:13 +01:00
jonaswinkler
118149c539 more translation work 2020-12-30 11:33:56 +01:00
Jonas Winkler
57da323cea Update README.md 2020-12-30 03:39:25 +01:00
jonaswinkler
db4b621631 angular message file 2020-12-30 01:48:06 +01:00
Jonas Winkler
5c7b65163e Merge pull request #214 from C0nsultant/patch-2
Fix ENV var name for user args in example config
2020-12-30 00:00:11 +01:00
Fabian Koller
9f18d0ad45 Fix ENV var name for user args in example config
The actual string used when looking up the user arguments ends with an S: `PAPERLESS_OCR_USER_ARGS`
2020-12-29 23:52:27 +01:00
jonaswinkler
2de3894d67 more localization tags 2020-12-29 23:41:59 +01:00
jonaswinkler
03c6a4e18e more localization #123 2020-12-29 23:37:33 +01:00
jonaswinkler
f9ab8d3b35 more localization tags #123 2020-12-29 23:34:04 +01:00
jonaswinkler
ef63ec40d9 changelog 2020-12-29 22:02:03 +01:00
jonaswinkler
761a6a4264 plural form fix 2020-12-29 22:01:56 +01:00
jonaswinkler
8139ecfd39 version bump 2020-12-29 22:01:37 +01:00
jonaswinkler
fb09f67899 bugfixes 2020-12-29 22:01:18 +01:00
jonaswinkler
d690b34ee0 added invalid PDF document with BOM marker 2020-12-29 21:02:45 +01:00
jonaswinkler
e24b40de29 more tests 2020-12-29 21:01:18 +01:00
jonaswinkler
6581cff8dc more tests 2020-12-29 20:55:27 +01:00
jonaswinkler
5c3ae44021 fix up gunicorn conf 2020-12-29 18:23:08 +01:00
jonaswinkler
2b341afcae fix some bugs with the filter editor 2020-12-29 17:32:56 +01:00
jonaswinkler
7f4cfc0b76 fix a couple issues with the bulk editor 2020-12-29 17:20:45 +01:00
jonaswinkler
b2327d6fde more settings 2020-12-29 17:09:07 +01:00
jonaswinkler
f964dd5935 added configuration option for the font #197 #207 2020-12-29 12:26:41 +01:00
jonaswinkler
39d1c051cf use the actual name of the package #205 2020-12-29 02:25:39 +01:00
Jonas Winkler
5622e13802 Merge pull request #206 from MarcoBuster/fix-missing-libxslt-dev
Add missing libxslt-dev
2020-12-29 02:24:15 +01:00
Marco Aceti
d6285cc851 Add libxslt-dev library to Dockerfile build 2020-12-29 01:57:46 +01:00
jonaswinkler
6b708c5ed3 Merge branch 'patch-2' of https://github.com/jovandeginste/paperless-ng into jovandeginste-patch-2 2020-12-29 01:08:46 +01:00
jonaswinkler
1dd2386fa5 messed up, restore functionality 2020-12-28 23:53:18 +01:00
jonaswinkler
bd01b821ec possible fix for #201 2020-12-28 23:24:08 +01:00
jonaswinkler
9093ac5901 more localization tags #123 2020-12-28 22:54:49 +01:00
Jo Vandeginste
755da317ea Update settings.py 2020-12-28 22:37:53 +01:00
Jo Vandeginste
6834c70bae Allow extending INSTALLED_APPS via environment
This allows a user to add "apps" (aka parsers) through the environment.

Especially useful when using Docker, and adding a test-parser.

Usage:

```yaml
services:
  webserver:
    environment:
      PAPERLESS_APPS: paperless_tika.apps.PaperlessTikaConfig
```

You can add more by separating them with a `,`:

```yaml
PAPERLESS_APPS: app1,app2
```
2020-12-28 22:19:30 +01:00
jonaswinkler
bd3a2306d6 better toasts 2020-12-28 21:52:09 +01:00
jonaswinkler
27666da4e9 more translation tags #123 2020-12-28 21:29:28 +01:00
jonaswinkler
7d86ee32af Merge branch 'master' into dev 2020-12-28 17:25:00 +01:00
jonaswinkler
70c02a1c82 fixes #185 2020-12-28 17:20:07 +01:00
jonaswinkler
39637fc4aa fixes #175 2020-12-28 17:09:19 +01:00
jonaswinkler
1de7a490b4 #186 allow filtering for documents with no correspondents / tags / types 2020-12-28 17:04:53 +01:00
jonaswinkler
6d4aa76405 fixes #196 2020-12-28 15:59:06 +01:00
jonaswinkler
27ae4f6b1e fixes #197 2020-12-28 15:48:35 +01:00
jonaswinkler
d6e733c56f add more localization tags #123 2020-12-28 15:39:53 +01:00
jonaswinkler
aa6e96e54d fix pycodestyle 2020-12-28 13:33:58 +01:00
jonaswinkler
3d173a13ab move the two post bulk edit tasks into one 2020-12-28 13:31:22 +01:00
jonaswinkler
7beb8a0929 fix enter select 2020-12-28 12:51:09 +01:00
jonaswinkler
8af0259671 rework the bulk editor 2020-12-28 12:36:26 +01:00
jonaswinkler
67953c98a9 remove some non-required stuff 2020-12-28 12:32:36 +01:00
jonaswinkler
527c533958 improve performance of the toggle dropdown button 2020-12-28 12:32:21 +01:00
jonaswinkler
544ca8d008 add ability to manually clear the cache on matching models 2020-12-28 12:31:50 +01:00
jonaswinkler
bd02c78966 fix the filter pipe 2020-12-28 12:31:30 +01:00
jonaswinkler
9e311241b3 rename some stuff 2020-12-28 12:31:14 +01:00
jonaswinkler
e228e18f04 it must have been late when I tried to do this 2020-12-28 12:29:53 +01:00
jonaswinkler
e2bea3aee3 add missing index task 2020-12-28 12:29:34 +01:00
Jonas Winkler
08beaf81d5 Update README.md 2020-12-28 12:03:30 +01:00
jonaswinkler
4fb5dce5e7 selection model 2020-12-28 00:52:28 +01:00
jonaswinkler
b8e7506de4 partial selection model implementation 2020-12-27 23:55:19 +01:00
Jonas Winkler
131ebf0480 Update README.md 2020-12-27 17:07:33 +01:00
jonaswinkler
80420a99f5 Merge branch 'dev' into feature-bulk-edit 2020-12-27 17:06:17 +01:00
jonaswinkler
6a70369a77 update index after bulk edit operations #195 2020-12-27 17:05:35 +01:00
jonaswinkler
fb83069975 fix test case. 2020-12-27 14:50:57 +01:00
jonaswinkler
f61ecadf76 Merge branch 'dev' of github.com:jonaswinkler/paperless-ng into dev 2020-12-27 14:47:40 +01:00
Jonas Winkler
f040c4e593 Merge pull request #192 from shamoon/fix/ios-safari-input-zoom
Avoid ios safari input zoom
2020-12-27 14:46:20 +01:00
jonaswinkler
c2a47ca4b1 remove "selectionSpansPages" 2020-12-27 13:34:54 +01:00
jonaswinkler
a283b815ef refactor 2020-12-27 13:34:36 +01:00
jonaswinkler
3dd4583ea8 always show count badges 2020-12-27 13:23:11 +01:00
jonaswinkler
bf79b252ad remove "Remove All" 2020-12-27 13:16:37 +01:00
jonaswinkler
a2ad62b310 typing 2020-12-27 13:10:43 +01:00
jonaswinkler
4b9a8f3409 move document count into matching model 2020-12-27 13:07:58 +01:00
jonaswinkler
802bd7fb0d client support for selection data 2020-12-27 12:54:47 +01:00
jonaswinkler
320298e3ff add api method to get selection data 2020-12-27 12:43:05 +01:00
Michael Shamoon
5e5059b2e7 Prevent iOS input zoom 2020-12-26 21:16:12 -08:00
Jonas Winkler
d6d7528668 Merge branch 'growse-patch-2' into dev 2020-12-27 01:49:12 +01:00
Andrew Rowson
3f8c74c4af Updated bind param gunicorn config file to listen on ipv6 2020-12-26 11:53:29 +00:00
jonaswinkler
0a1b810da7 Merge branch 'dev' into feature-bulk-edit 2020-12-26 01:09:12 +01:00
jonaswinkler
25fb9fb185 better selection 2020-12-26 01:08:54 +01:00
jonaswinkler
c547128238 Merge branch 'dev' into feature-bulk-edit 2020-12-25 22:19:10 +01:00
jonaswinkler
99b5e25731 clarify error messages #176 2020-12-25 19:26:27 +01:00
jonaswinkler
20e724da46 fixes #182 2020-12-25 19:24:39 +01:00
jonaswinkler
03838421bc not sure when that got in here 2020-12-25 19:23:53 +01:00
jonaswinkler
b9cf517cd1 front end client support for filtering for no correspondent/document type 2020-12-25 19:06:12 +01:00
jonaswinkler
f7f78d80b7 added isnull filters for document types and correspondents 2020-12-25 19:01:46 +01:00
Jonas Winkler
e63b820c07 Merge pull request #189 from shamoon/feature-bulk-editor
Refactored bulk editor
2020-12-25 16:11:31 +01:00
Michael Shamoon
2ac6a02e31 More simplification 2020-12-25 01:14:56 -08:00
Michael Shamoon
6e79b771ec Refactor bulk editor to be self-contained
See https://github.com/jonaswinkler/paperless-ng/pull/162#issuecomment-750425915
2020-12-25 00:58:17 -08:00
Andrew Rowson
37caf6a64a Gunicorn should bind to both ipv4 and ipv6
As per https://docs.gunicorn.org/en/stable/settings.html#bind
2020-12-24 11:53:20 +00:00
jonaswinkler
198354d07d a couple small changes 2020-12-23 22:45:54 +01:00
jonaswinkler
eec9716ffa more translation markers 2020-12-23 22:34:30 +01:00
jonaswinkler
64d0c7fae6 silence compiler warnings 2020-12-23 18:07:37 +01:00
Jonas Winkler
c54d26ed19 Update README.md 2020-12-23 16:09:13 +01:00
jonaswinkler
cffe9fa354 debug log mime type #176 2020-12-23 15:22:28 +01:00
jonaswinkler
46b0776714 fix typo #176 2020-12-23 15:14:24 +01:00
jonaswinkler
929b25a969 add api support for adding and removing many tags simultaneously 2020-12-23 15:13:55 +01:00
jonaswinkler
3c2fac3d28 Merge branch 'feature-bulk-edit' of github.com:jonaswinkler/paperless-ng into feature-bulk-edit 2020-12-23 15:12:50 +01:00
jonaswinkler
9a165e87fd Merge branch 'master' into dev 2020-12-23 15:09:58 +01:00
jonaswinkler
95c4e77ae4 added many localization markers to the front end #123 2020-12-23 15:09:39 +01:00
Jonas Winkler
ba3696566f Merge pull request #162 from shamoon/feature-bulk-editor
Bulk editing UI
2020-12-23 14:42:20 +01:00
Jonas Winkler
6589369e1b Update README.md 2020-12-23 02:29:58 +01:00
Jonas Winkler
9c2c74ad2b Update README.md 2020-12-23 01:51:38 +01:00
jonaswinkler
b7c118afa3 more tests 2020-12-22 21:08:54 +01:00
jonaswinkler
e9b5f8d9f8 api validation, more tests 2020-12-22 20:28:41 +01:00
Jonas Winkler
7d676a75a8 Update README.md 2020-12-22 17:26:07 +01:00
jonaswinkler
544e8db722 error in test case 2020-12-22 17:08:30 +01:00
Michael Shamoon
ba066af664 Merge branch 'feature-bulk-edit' into feature-bulk-editor 2020-12-22 07:43:51 -08:00
Jonas Winkler
c4286d0a48 Update README.md 2020-12-22 16:33:41 +01:00
jonaswinkler
1c535e6ff1 Merge branch 'master' of github.com:jonaswinkler/paperless-ng 2020-12-22 15:58:24 +01:00
jonaswinkler
0b9ea5c60f Merge branch 'dev' 2020-12-22 15:58:06 +01:00
jonaswinkler
0fa3c3188d update index after import 2020-12-22 15:53:04 +01:00
jonaswinkler
ebcb2cf694 progress bar for the document importer 2020-12-22 15:50:27 +01:00
jonaswinkler
8dc15352ef changelog layout 2020-12-22 15:22:24 +01:00
jonaswinkler
403de42031 discard selection when switching views 2020-12-22 15:18:42 +01:00
jonaswinkler
b08a5e62d8 documentation 2020-12-22 14:43:16 +01:00
jonaswinkler
978bb3c339 more change log 2020-12-22 14:13:58 +01:00
jonaswinkler
3e389a3140 versions 2020-12-22 14:09:38 +01:00
jonaswinkler
0dbd598116 changelog 2020-12-22 13:54:41 +01:00
jonaswinkler
01d10da061 Merge branch 'dev' into feature-bulk-edit 2020-12-22 13:40:24 +01:00
jonaswinkler
0aad31b4bc fixes clearing fields that should not be clearable 2020-12-22 13:39:46 +01:00
jonaswinkler
2f599ca1d3 fixes an issue with open documents not refreshing after bulk operations 2020-12-22 13:32:30 +01:00
jonaswinkler
cd8f99d2c3 Merge branch 'dev' into feature-bulk-edit 2020-12-22 13:04:45 +01:00
jonaswinkler
6968e228e1 increase indexing speed 2020-12-22 13:04:22 +01:00
jonaswinkler
b7d310ef90 more tests 2020-12-22 13:04:08 +01:00
jonaswinkler
1c3b85249c more tests 2020-12-22 13:03:50 +01:00
jonaswinkler
147a8774c6 more tests 2020-12-22 12:40:06 +01:00
Michael Shamoon
406f8daa12 TODO comment 2020-12-22 03:32:51 -08:00
Michael Shamoon
0a4a06b991 Singular items dont need removal logic since dropdowns dont support this (yet?) 2020-12-22 03:13:57 -08:00
Michael Shamoon
41e842a9e6 Unify bulk operations logic, working tags assignment and removal 2020-12-22 02:58:20 -08:00
Michael Shamoon
1fb3316436 Fix remove all operations 2020-12-22 02:18:05 -08:00
Michael Shamoon
2f26d07480 Only pass one item to bulk methods for correspondents/doctypes 2020-12-22 02:14:37 -08:00
Michael Shamoon
360fc081bb Completely rewrite change detection, add back remove operations 2020-12-22 02:08:36 -08:00
Michael Shamoon
4cae338479 Merge branch 'feature-bulk-edit' into feature-bulk-editor 2020-12-22 00:19:35 -08:00
jonaswinkler
9ace199422 Merge branch 'dev' into feature-bulk-edit 2020-12-22 03:00:22 +01:00
jonaswinkler
f42e5bf1e5 added tracking to the document list 2020-12-22 02:59:09 +01:00
jonaswinkler
5863060585 added selection to the large cards 2020-12-22 02:43:18 +01:00
jonaswinkler
e466158ce2 bugfix 2020-12-22 01:17:07 +01:00
jonaswinkler
38156123d4 Merge branch 'dev' into feature-bulk-edit 2020-12-21 23:20:55 +01:00
jonaswinkler
ae3e2a7063 updated documentation 2020-12-21 21:28:21 +01:00
Michael Shamoon
48d83e166b use ngIf for editor components to maybe help performance 2020-12-21 11:12:48 -08:00
Michael Shamoon
29e9d7d793 Some changes from #141 were lost 2020-12-21 09:53:43 -08:00
Michael Shamoon
dcbc0ea2e5 Revert "Fix list small card layout"
This reverts commit 0e93f7eba5.
2020-12-21 09:52:55 -08:00
jonaswinkler
c527b274b6 disallow clearing tag color 2020-12-21 18:23:06 +01:00
jonaswinkler
5892941d8a updated tag coloring for easier readability 2020-12-21 18:17:21 +01:00
jonaswinkler
b653e44f65 changed field order, updated ng-select for tag color selection 2020-12-21 18:15:28 +01:00
jonaswinkler
e75534c0f2 fixes #165 2020-12-21 17:35:05 +01:00
Michael Shamoon
0e93f7eba5 Fix list small card layout
Some changes from #141 were lost
2020-12-21 08:14:06 -08:00
jonaswinkler
665863e395 Display name of current user on the dashboard 2020-12-20 17:18:23 +01:00
Michael Shamoon
d9e3895f34 Fix 2px difference between two editors =) 2020-12-20 08:07:19 -08:00
Michael Shamoon
f06e2c1089 Merge remote-tracking branch 'upstream/dev' into feature-bulk-editor 2020-12-20 07:49:27 -08:00
jonaswinkler
c6b9e2b544 revert last commit 2020-12-20 16:00:11 +01:00
jonaswinkler
240d5b9da2 reorganized docker build. 2020-12-20 15:59:37 +01:00
jonaswinkler
b10e7abbe8 don't know how that got in here 2020-12-20 15:25:47 +01:00
jonaswinkler
01c2fe508e more tests 2020-12-20 14:39:17 +01:00
jonaswinkler
ee31fdc650 removed unused code 2020-12-20 14:00:24 +01:00
Michael Shamoon
fa7b90a584 Add apply button to dropdowns 2020-12-20 01:04:54 -08:00
Michael Shamoon
37c2051e01 Typos & code fixes 2020-12-20 00:37:37 -08:00
Michael Shamoon
3561935e1b Fix some TS syntax errors 2020-12-20 00:12:40 -08:00
Michael Shamoon
7669679fb1 Mobile fixes 2020-12-19 23:49:20 -08:00
Michael Shamoon
2e44c18cdb Show 's' when needed instead of (s) on document list 2020-12-19 23:44:33 -08:00
Michael Shamoon
86da578774 Hide toggled state & counts when selection spans documents not in view 2020-12-19 23:42:08 -08:00
Michael Shamoon
46f778111c Live filter badges when editing & hide badges when active filter items in dropdown 2020-12-19 22:54:37 -08:00
Michael Shamoon
7dfcc7f47b Refactor dropdown button component 2020-12-19 22:31:14 -08:00
Michael Shamoon
39b35c090b Hide filter / bulk editor for better switching 2020-12-19 22:14:52 -08:00
Michael Shamoon
ee4e026ba2 rework toolbar 2020-12-19 22:06:17 -08:00
Michael Shamoon
de6ba3489a Cleanup dropdown button component 2020-12-19 21:56:31 -08:00
Michael Shamoon
6381093386 Refactor SlectableItem to ToggleableItem 2020-12-19 21:52:45 -08:00
Michael Shamoon
fd6bfd02ce SelectableItem dropdowns refactoring 2020-12-19 21:45:53 -08:00
Michael Shamoon
1da652ba4d Only apply edits when something has changed 2020-12-19 21:42:19 -08:00
Michael Shamoon
9a4190bedf Rename dropdown type enum & add pass as variable to use like an Enum 2020-12-19 20:19:37 -08:00
Michael Shamoon
561db8607a Unused bulk tag functions 2020-12-19 15:20:07 -08:00
jonaswinkler
7f9a0204b5 removed most of the logic that extracts data from filename patterns #156 2020-12-20 00:08:05 +01:00
jonaswinkler
32224f187d test CONSUMER_DELETE_DUPLICATES 2020-12-20 00:06:33 +01:00
Michael Shamoon
d91fa99e77 Handler for bulk set tags (awaiting API endpoint) 2020-12-19 14:26:26 -08:00
jonaswinkler
3f94fc2618 fix & test a migration 2020-12-19 21:52:58 +01:00
jonaswinkler
e79c45c98d fix test cases 2020-12-19 20:39:56 +01:00
Michael Shamoon
400da7bbc5 Minor refactoring 2020-12-19 07:49:32 -08:00
jonaswinkler
bb814da95b more test. 2020-12-19 16:46:09 +01:00
jonaswinkler
fad3df1e39 removed x-frame-options, since that was only used for the <object> pdf display tag. 2020-12-19 16:46:04 +01:00
jonaswinkler
1b1b57eb6a more tests 2020-12-19 15:54:13 +01:00
jonaswinkler
57a5a4147b test case 2020-12-19 14:48:42 +01:00
Jonas Winkler
1041504cb1 Merge pull request #159 from Skylinar/master
Add 'Epson WF-7710DWF' to scanner.rst
2020-12-19 13:49:17 +01:00
Jonas Winkler
26b40e06f8 Merge branch 'master' into master 2020-12-19 13:49:01 +01:00
Skylinar
df8235de13 Update scanners.rst
typo
2020-12-19 13:36:10 +01:00
Skylinar
12e45624db Add 'Epson WF-7710DWF' to scanner.rst 2020-12-19 13:32:31 +01:00
jonaswinkler
37237dfcf6 default title 2020-12-19 12:46:11 +01:00
Michael Shamoon
da3695e3a4 Working to backend except tags 2020-12-19 02:26:41 -08:00
Michael Shamoon
24c53e78a7 Working bulk editor component 2020-12-19 02:08:33 -08:00
Michael Shamoon
275bd96ba8 Refactor filterable dropdowns to allow intermediate state 2020-12-19 00:13:08 -08:00
Michael Shamoon
01d448ecde Bulk editor component skeleton 2020-12-18 21:19:49 -08:00
jonaswinkler
3cdd38cb70 Merge branch 'feature-more-like-this' into dev 2020-12-19 01:15:51 +01:00
jonaswinkler
f184e6b162 fix some layout issues 2020-12-19 01:15:40 +01:00
Michael Shamoon
55a6dca373 Refactor editor and filterable dropdowns to be common components in anticipation of bulk editor 2020-12-18 16:03:52 -08:00
jonaswinkler
5517c049b8 Merge branch 'dev' into feature-more-like-this 2020-12-19 00:48:31 +01:00
Jonas Winkler
86f83f2bc4 Merge pull request #157 from shamoon/fix/issue-150
Add page number to document toolbar
2020-12-19 00:47:06 +01:00
jonaswinkler
557abbc17e Merge branch 'dev' into feature-more-like-this 2020-12-19 00:06:16 +01:00
Michael Shamoon
e0293db16d Only show page numbers when content type is application/pdf 2020-12-18 15:04:52 -08:00
Michael Shamoon
fbb2da42dc Merge branch 'dev' into feature-bulk-editor 2020-12-18 14:55:21 -08:00
Michael Shamoon
2d841e7167 Refactor 2020-12-18 14:47:06 -08:00
Michael Shamoon
f214fe1b3e Log line 2020-12-18 14:44:17 -08:00
Michael Shamoon
e5fe515b69 Add page number to toolbar 2020-12-18 14:31:09 -08:00
jonaswinkler
dfb88ebf83 removed the date hack. fixes #144
also refer to #148
2020-12-18 20:17:17 +01:00
jonaswinkler
789abb3bbb changed up the highlight fragment formatter 2020-12-18 16:42:33 +01:00
jonaswinkler
273c474e3f layout changes 2020-12-18 14:09:12 +01:00
Jonas Winkler
b262ec4b32 Merge pull request #154 from shamoon/fix/issue-152
Searchable dropdowns on document details component
2020-12-18 12:40:02 +01:00
Michael Shamoon
c05de3d57f Tiny padding fixes 2020-12-18 01:18:11 -08:00
Michael Shamoon
55c4c690ef Fix wrapping with multiple tags, embiggen tags, pretty icons 2020-12-18 01:13:30 -08:00
Michael Shamoon
e10a2391c4 Use ng-select for document detail screen tags 2020-12-18 00:53:01 -08:00
Michael Shamoon
9b244d0265 Use ng-select for document detail screen 2020-12-17 23:09:27 -08:00
Jonas Winkler
cfc1ca45fc Update README.md 2020-12-18 01:35:08 +01:00
Jonas Winkler
1c4e3f682e Update README.md 2020-12-18 01:31:46 +01:00
jonaswinkler
75b22e8684 Merge branch 'dev' into feature-bulk-edit 2020-12-18 00:38:31 +01:00
jonaswinkler
ca2cb694d0 code style 2020-12-18 00:10:16 +01:00
jonaswinkler
cea34211b6 Merge branch 'dev' into feature-more-like-this 2020-12-18 00:03:14 +01:00
jonaswinkler
d252d040bf Merge branch 'dev' of github.com:jonaswinkler/paperless-ng into dev 2020-12-18 00:02:53 +01:00
jonaswinkler
43c88ae0f0 Merge branch 'dev' into feature-more-like-this 2020-12-18 00:02:30 +01:00
jonaswinkler
93be4e98d5 scroll to top when searching again 2020-12-17 23:41:55 +01:00
jonaswinkler
659cd3e9d5 hide search controls on document list 2020-12-17 23:41:46 +01:00
jonaswinkler
2c3eaadbce test cases 2020-12-17 23:24:28 +01:00
Jonas Winkler
1947d67e69 Merge pull request #141 from shamoon/document-list-layout-tweaks
Document list small card layout adjustments
2020-12-17 22:08:48 +01:00
jonaswinkler
35dcc54dc8 fixes cookie_prefix for development setups 2020-12-17 21:54:10 +01:00
jonaswinkler
48796e6961 fixes #149 2020-12-17 21:46:56 +01:00
jonaswinkler
164418880a more like this searching 2020-12-17 21:36:21 +01:00
Michael Shamoon
fbca412d30 Add more card columns on very large screens 2020-12-16 16:18:41 -08:00
jonaswinkler
70347bb8f3 added a note to the documentation regarding character limits of PostgreSQL 2020-12-16 23:26:29 +01:00
jonaswinkler
eaf11ea134 changelog and versions 2020-12-16 22:39:13 +01:00
jonaswinkler
a59f8cd1b1 Merge branch 'master' into dev 2020-12-16 22:33:11 +01:00
jonaswinkler
e9affbc1cf revert the changes that caused issues in the admin. 2020-12-16 22:33:03 +01:00
jonaswinkler
aa8789ae31 fix up the migration for encrypted documents and a couple other associated issues. 2020-12-16 21:53:11 +01:00
jonaswinkler
5c310c51d4 fix up the migration for encrypted documents. 2020-12-16 21:08:41 +01:00
jonaswinkler
cf3fa50b55 these changes shouldn't have been commited at all. my bad. 2020-12-16 21:08:03 +01:00
jonaswinkler
7f933d373f fixes the decryption command not working. 2020-12-16 19:50:38 +01:00
jonaswinkler
ece94379d8 fixes #143 2020-12-16 19:35:21 +01:00
jonaswinkler
aa714bded3 Merge branch 'dev' 2020-12-16 18:44:57 +01:00
jonaswinkler
ecfae9dadd fixed some issues with the ordering, test cases and migrations. 2020-12-16 18:40:19 +01:00
jonaswinkler
69c04a209a changelog 2020-12-16 18:18:01 +01:00
jonaswinkler
e4ec52ed29 default saved view names 2020-12-16 18:16:14 +01:00
jonaswinkler
50aedc1094 fixed an issue with clickable types and correspondents on the docment table list 2020-12-16 17:52:43 +01:00
jonaswinkler
062f8e5a73 update save filter picture 2020-12-16 17:47:34 +01:00
jonaswinkler
c813c02025 version increment. 2020-12-16 17:01:20 +01:00
jonaswinkler
28f45d8f15 fixes an issue with the date dropdowns 2020-12-16 16:59:26 +01:00
jonaswinkler
8e339789fa more fixes regarding empty titles 2020-12-16 16:44:54 +01:00
jonaswinkler
dec17a3b9b fixes a one time issue when migrating to the new version. 2020-12-16 16:19:12 +01:00
jonaswinkler
6b60501dc7 changelog 2020-12-16 16:04:25 +01:00
jonaswinkler
0e78f32009 fixed an issue with the settings not saving in case no saved views are present 2020-12-16 16:04:20 +01:00
jonaswinkler
b13ec571f8 fixes #134 2020-12-16 14:41:57 +01:00
jonaswinkler
9f5e6d1969 fixes metadata display 2020-12-16 14:23:10 +01:00
jonaswinkler
b2e0a8c884 thumbnail generation 2020-12-16 14:19:11 +01:00
jonaswinkler
e47b105185 fixes #7 and some test cases. 2020-12-16 14:17:05 +01:00
jonaswinkler
8bd82f5c69 fixing some test case warnings, case insensitive sorting for tags, correspondents and types. 2020-12-16 13:49:48 +01:00
jonaswinkler
e528a587cc fixed some issues with the test cases. 2020-12-16 13:41:02 +01:00
jonaswinkler
1a526ac31e fixes #140 2020-12-16 02:12:58 +01:00
jonaswinkler
22e56f09ba fixes some issues regarding #139 2020-12-16 00:14:32 +01:00
jonaswinkler
af0eafbb3b Merge branch 'dev' of github.com:jonaswinkler/paperless-ng into dev 2020-12-15 23:44:23 +01:00
Jonas Winkler
8e57d4e791 Merge pull request #137 from shamoon/quick-filters-mobile-2
Mobile Improvements for "Quick Filters" (2)
2020-12-15 23:39:05 +01:00
jonaswinkler
b2a9cf4709 docs 2020-12-15 23:35:10 +01:00
Michael Shamoon
677cfb7a1e Use bootstrap row-cols-* classes to keep card list view full width 2020-12-15 14:31:18 -08:00
Michael Shamoon
fb9d750684 Delete button margin-left 2020-12-15 14:19:40 -08:00
Michael Shamoon
b8469946a8 Smaller editor, cleaned up responsive flow 2020-12-15 11:09:25 -08:00
Michael Shamoon
beffde1051 quick filter button badges 2020-12-15 07:10:31 -08:00
Jonas Winkler
71c58c4b05 Update README.md 2020-12-15 15:04:28 +01:00
Jonas Winkler
02e67d25b4 Update README.md 2020-12-15 15:03:34 +01:00
Jonas Winkler
d208ab1e12 Update README.md 2020-12-15 15:03:00 +01:00
jonaswinkler
55075619c1 documentation 2020-12-15 14:43:07 +01:00
jonaswinkler
7dce57b9f7 changelog 2020-12-15 14:34:48 +01:00
jonaswinkler
a0c74025e3 changelog and docs 2020-12-15 14:30:31 +01:00
jonaswinkler
31bea6a361 path sanitation 2020-12-15 14:11:05 +01:00
jonaswinkler
5894060dc5 fixes #25 2020-12-15 13:52:35 +01:00
jonaswinkler
b787983e42 more tests 2020-12-15 13:47:43 +01:00
jonaswinkler
7e0aa7136a more tests 2020-12-15 13:26:01 +01:00
jonaswinkler
56204933b0 bugfix, tests 2020-12-15 13:16:28 +01:00
jonaswinkler
49be87fe37 code style 2020-12-15 12:06:24 +01:00
Michael Shamoon
03f071fd27 Styled, organized button UI 2020-12-15 00:57:31 -08:00
Michael Shamoon
34c42c4339 Better svgs 2020-12-14 23:39:10 -08:00
Michael Shamoon
b45bd66573 Basic bulk editor component 2020-12-14 23:14:19 -08:00
Michael Shamoon
3b2bc292d8 Tweak checkbox 2020-12-14 23:14:04 -08:00
Michael Shamoon
30185d560c Much cleaner way to set icon 2020-12-14 22:33:50 -08:00
Michael Shamoon
6e614eda5e Merge branch 'dev' into quick-filters-mobile-2 2020-12-14 19:45:28 -08:00
Michael Shamoon
164755c755 Breakpoints for screen sizes, icons for mobile 2020-12-14 19:45:22 -08:00
jonaswinkler
6d39dfeb3b forgot to address this. 2020-12-15 03:53:18 +01:00
jonaswinkler
999b36473c more refactoring and bug fixing. 2020-12-15 03:29:23 +01:00
jonaswinkler
8b57967836 Merge branch 'dev' into feature-bulk-edit 2020-12-15 03:13:22 +01:00
jonaswinkler
ff71b04848 editable saved views 2020-12-15 02:35:04 +01:00
jonaswinkler
67d03c11b9 fixed the date selection dropdowns.
- They still contain that ugly hack.
2020-12-15 00:48:06 +01:00
jonaswinkler
533be7e96e better highlight of active filters 2020-12-15 00:30:36 +01:00
jonaswinkler
4ed56e4603 fix 2020-12-15 00:00:40 +01:00
jonaswinkler
45848f5e34 removed manual date formatting/parsing 2020-12-14 22:46:50 +01:00
jonaswinkler
cf619d9d31 typing 2020-12-14 22:20:28 +01:00
jonaswinkler
de87efc291 confirmation messages 2020-12-14 21:14:33 +01:00
jonaswinkler
889fe5890d refactored titles 2020-12-14 20:59:18 +01:00
jonaswinkler
958acd8a36 imports 2020-12-14 20:21:31 +01:00
jonaswinkler
381a503947 bugfix 2020-12-14 19:39:16 +01:00
jonaswinkler
b7126030d1 many changes to support server side saved views 2020-12-14 19:26:36 +01:00
jonaswinkler
13d934dc6e new saved view service replaces old local storage based service 2020-12-14 18:46:11 +01:00
jonaswinkler
926e746005 Merge branch 'dev' into feature-server-side-saved-views 2020-12-14 16:52:51 +01:00
jonaswinkler
94c07839a4 refactored filter service
- I wasn't too happy with that in the end.
- The filter editor should not be concerned about managing filter rule state.
- Therefore, it should not access a service for filter rules.
- The editor should simply be given a set of rules, and edit that rule set.
- The only entity that should manage filter state should be the document list service, and the saved view service in the form of filters associated with saved views.
2020-12-14 16:51:01 +01:00
jonaswinkler
277e668e07 Merge branch 'dev' into feature-server-side-saved-views 2020-12-14 11:49:03 +01:00
jonaswinkler
10440ec820 this button wasn't really doing anything. 2020-12-14 11:31:12 +01:00
jonaswinkler
02c1d496d6 some refactoring. 2020-12-14 11:22:24 +01:00
jonaswinkler
98ab79ad5a fix title filter not removing filter rule 2020-12-14 11:12:36 +01:00
Jonas Winkler
e1fef59e82 Merge pull request #111 from shamoon/quick-filters
Quick filters
2020-12-14 11:02:34 +01:00
Michael Shamoon
32201dd034 button badge margin 2020-12-13 20:56:49 -08:00
Michael Shamoon
670b6d3629 Change date filter active check to check circle filled 2020-12-13 20:53:00 -08:00
Michael Shamoon
54d90a4c4b Code cleanup 2020-12-13 20:37:13 -08:00
Michael Shamoon
9bfc92cf79 Fix missing NgbDropdown import 2020-12-13 20:16:23 -08:00
Michael Shamoon
a12ec00827 Remove unused displayName 2020-12-13 20:16:15 -08:00
jonaswinkler
8cc0336338 prevent usage of {tags} directly. 2020-12-14 03:01:50 +01:00
jonaswinkler
bad7caa8b9 fixes #117 2020-12-14 02:46:46 +01:00
jonaswinkler
251fc582e9 fixes #130 2020-12-14 02:19:53 +01:00
Michael Shamoon
245af65841 Auto-close menu when single item chosen with Enter key 2020-12-13 15:47:46 -08:00
Michael Shamoon
ee7492cf52 Clear date filter buttons 2020-12-13 15:38:28 -08:00
Michael Shamoon
8e5c2a2b14 Fix date clearing 2020-12-13 15:08:45 -08:00
jonaswinkler
1343e4c99f Merge branch 'dev' of github.com:jonaswinkler/paperless-ng into dev 2020-12-13 23:47:01 +01:00
jonaswinkler
89cb1211e7 docs 2020-12-13 23:46:48 +01:00
Michael Shamoon
04bb7d4893 Remove card around filter editor 2020-12-13 14:46:10 -08:00
Michael Shamoon
1fafb9ace6 Prettier styling on dropdowns 2020-12-13 14:40:17 -08:00
Michael Shamoon
ae51619243 Make date buttons same as other dropdowns 2020-12-13 14:11:43 -08:00
Michael Shamoon
7ac101d84e Typo! 2020-12-13 12:17:01 -08:00
Michael Shamoon
2c18f6268b Comment cleanup 2020-12-13 11:34:58 -08:00
Michael Shamoon
7d212f6e80 Last time fixing the toggling logic, I hope =/ 2020-12-13 11:33:57 -08:00
Michael Shamoon
3d8cd0f0d6 change tag selected marker to badge 2020-12-13 11:32:13 -08:00
Michael Shamoon
3f719a21e0 Typo from merge 2020-12-13 11:29:12 -08:00
Michael Shamoon
7a75d3f41b Merge branch 'dev' into quick-filters 2020-12-13 11:27:40 -08:00
Michael Shamoon
1ddad84985 Fix visual clearing of date field 2020-12-13 11:23:21 -08:00
Michael Shamoon
bcdbc975d6 Show filter has items selected 2020-12-13 11:20:28 -08:00
Michael Shamoon
a61ea3555a Ok now toggling logic is fixed 2020-12-13 11:17:10 -08:00
Michael Shamoon
2de546fd52 Fix tag / correspondent / document type toggling logic 2020-12-13 11:03:50 -08:00
Michael Shamoon
d6894d3c64 Change views menu title 2020-12-13 10:28:09 -08:00
Michael Shamoon
bb1725c7dd Typescript cleanup 2020-12-13 10:25:51 -08:00
Michael Shamoon
6f684f8070 Dropdown components now accept lists not observables 2020-12-13 10:24:20 -08:00
Michael Shamoon
7712230300 remove unneeded display Input 2020-12-13 10:18:03 -08:00
Jonas Winkler
7e4c5af158 Merge pull request #133 from rYR79435/patch-2
Open GitHub and Documentation links in a new tab
2020-12-13 15:48:57 +01:00
jonaswinkler
2dc3019083 table selection highlighting 2020-12-13 15:28:20 +01:00
jonaswinkler
b5a85caa72 confirm dialogs for remove operations 2020-12-13 15:20:24 +01:00
jonaswinkler
bb9b438aa6 Merge branch 'dev' into feature-bulk-edit 2020-12-13 15:13:43 +01:00
jonaswinkler
3089b049cf refactored metadata views 2020-12-13 14:56:44 +01:00
jonaswinkler
5bea5e75c0 Refactored delete dialog into a more generic confirm dialog 2020-12-13 14:28:37 +01:00
jonaswinkler
7906d8fef1 selection for small cards 2020-12-13 14:10:55 +01:00
rYR79435
30853e963e Open GitHub and Documentation links in a new tab 2020-12-13 13:30:30 +01:00
Michael Shamoon
1379c039b8 Workaround for infinte loop breaks two way binding for date picker initialization 2020-12-13 02:03:59 -08:00
Michael Shamoon
37c21e518d set max date for date pickers 2020-12-13 01:27:11 -08:00
Michael Shamoon
e215e11417 Completely refactored because programming
Extracted filter editor to service
Made all components actually reactive
2020-12-12 22:53:34 -08:00
jonaswinkler
2374506a20 Merge branch 'dev' into feature-bulk-edit 2020-12-13 00:52:36 +01:00
jonaswinkler
1c4d19198f a couple adjustments for the document viewer. 2020-12-12 22:56:44 +01:00
jonaswinkler
e77be6a2ed Merge branch 'dev' of github.com:jonaswinkler/paperless-ng into dev 2020-12-12 20:43:20 +01:00
Jonas Winkler
1fdf1ef337 Merge pull request #129 from shamoon/fix/issue-65
Change PDF rendering
2020-12-12 20:41:54 +01:00
jonaswinkler
f5cc5fbaa3 made the file renamer somewhat faster. 2020-12-12 20:32:11 +01:00
jonaswinkler
a0631413d6 fixes bauerj/paperless_app#23 and most of all other scanner apps out there. 2020-12-12 18:25:15 +01:00
Michael Shamoon
8ce4434ba9 Move date dropdown component 2020-12-12 09:01:48 -08:00
Michael Shamoon
f6a50ee7c6 Bottom margin on columns for mobile stacking 2020-12-12 08:50:52 -08:00
Michael Shamoon
beff45a835 Fix PDF column width layout issues 2020-12-12 08:43:03 -08:00
jonaswinkler
dfa1f29809 add backend support for saved views 2020-12-12 15:46:56 +01:00
Michael Shamoon
2a57f9d6e5 NgbDate comparison error 2020-12-12 02:14:15 -08:00
Michael Shamoon
02871e1e22 Date filter clearing 2020-12-12 02:07:25 -08:00
Michael Shamoon
9cd40e96f4 Working date filtering 2020-12-12 01:09:52 -08:00
jonaswinkler
bf9051e44d made a serious mistake. fixed. 2020-12-12 02:06:43 +01:00
jonaswinkler
ebb39b13f0 tests 2020-12-12 01:23:26 +01:00
jonaswinkler
4f14e0f425 fixes #125 2020-12-12 01:19:22 +01:00
Michael Shamoon
e7cb358536 Fix broken card tags / correspondent links 2020-12-11 15:20:47 -08:00
jonaswinkler
a85792e327 tests. 2020-12-11 23:34:34 +01:00
jonaswinkler
f5df910894 document list validation. 2020-12-11 23:34:24 +01:00
jonaswinkler
80b47fa287 codestyle 2020-12-11 23:34:14 +01:00
Michael Shamoon
1aa76f8483 Merge branch 'dev' into fix/issue-65 2020-12-11 14:29:26 -08:00
Michael Shamoon
de5d360d52 Use ng2-pdf-viewer
And remove now-unused safeUrl pipe
2020-12-11 14:22:02 -08:00
jonaswinkler
f0ad92e542 Merge branch 'dev' into feature-bulk-edit 2020-12-11 20:14:48 +01:00
jonaswinkler
0b7ffa31d1 fixes #115 2020-12-11 17:57:56 +01:00
jonaswinkler
b452816a29 fixes #122 2020-12-11 17:49:32 +01:00
jonaswinkler
d1d09ac6ac checboxes for small cards. does not work yet. 2020-12-11 17:35:21 +01:00
jonaswinkler
66240188c7 import fix 2020-12-11 14:51:20 +01:00
jonaswinkler
d1f285113d bulk edit menu and methods 2020-12-11 14:49:22 +01:00
jonaswinkler
56dfc71bb9 document list service: selection model 2020-12-11 14:48:33 +01:00
jonaswinkler
a8f27f79dd delete dialog: delay enable delete button 2020-12-11 14:47:33 +01:00
jonaswinkler
2c702eb568 fixed some issues with the data access service, support for requesting all IDs of filtered documents (required for selection) 2020-12-11 14:46:48 +01:00
jonaswinkler
63a58ccc38 a simple dialog that selects tags/correspondents/types 2020-12-11 14:30:59 +01:00
jonaswinkler
4b0027797a bulk edit view 2020-12-11 14:30:18 +01:00
jonaswinkler
66d6d29c23 add support to the documents api to only serve selected fields 2020-12-11 14:29:43 +01:00
jonaswinkler
fbb3a069cd add bulk editing methods 2020-12-11 14:28:07 +01:00
Michael Shamoon
a37796d0cf Allow enter key to toggle items in filtered list if single item remains 2020-12-11 01:40:42 -08:00
Michael Shamoon
0b4c860354 refactoring 2020-12-11 01:19:49 -08:00
Michael Shamoon
a4a08aa667 auto-select list filter field & clear on close 2020-12-11 01:16:58 -08:00
Michael Shamoon
c24bfd4d2b filter-dropdown-date rough implementation 2020-12-11 01:03:05 -08:00
Michael Shamoon
ed480f62e3 filter-rule-type displayName property 2020-12-11 00:08:06 -08:00
Michael Shamoon
66aa7319ab Small text 2020-12-11 00:07:26 -08:00
Michael Shamoon
4146955f4a Shadows! 2020-12-10 15:51:11 -08:00
Michael Shamoon
57504b7ee6 Display tags with color pills 2020-12-10 15:49:00 -08:00
Michael Shamoon
364df5c050 Fix toggling off active items 2020-12-10 15:37:56 -08:00
Michael Shamoon
f83185bfe4 Refactored dropdowns allow clearing, active checkmarks 2020-12-10 15:36:17 -08:00
Michael Shamoon
db02d68a5a Refactored dropdowns to separate component 2020-12-10 14:41:37 -08:00
Michael Shamoon
9a18954686 Fix unused method event parameter 2020-12-10 09:24:07 -08:00
Michael Shamoon
edcb62476b Remove clear button outline 2020-12-10 09:13:26 -08:00
Jonas Winkler
3a82b7806a Update README.md 2020-12-10 16:28:02 +01:00
jonaswinkler
51b1528fee Merge branch 'dev' into feature-bulk-edit 2020-12-10 16:26:16 +01:00
jonaswinkler
defa80d05a fixes #91 2020-12-10 16:25:34 +01:00
jonaswinkler
abd54eeb3a Merge branch 'dev' into feature-bulk-edit 2020-12-10 15:56:03 +01:00
jonaswinkler
2f7bb01f34 moved metadata extraction to the parsers 2020-12-10 14:57:53 +01:00
Michael Shamoon
25e1177198 Title filtering 2020-12-10 01:42:40 -08:00
Michael Shamoon
ab8a1cfded Working moved dropdowns 2020-12-10 00:46:13 -08:00
Michael Shamoon
fa5121082d Moved quick filters to filter editor 2020-12-09 23:12:51 -08:00
Michael Shamoon
f0d86130ec Use tag component for tag colors etc 2020-12-09 01:52:44 -08:00
Michael Shamoon
ed236460b5 Fix document type search field placeholder 2020-12-09 01:36:33 -08:00
Michael Shamoon
a4f7c5ddcb Unused test code 2020-12-09 01:34:09 -08:00
Michael Shamoon
4fbb814e5b Visual tweaks 2020-12-09 01:28:04 -08:00
Michael Shamoon
0f635d1bb2 Clear button & visual tweaks 2020-12-09 01:28:04 -08:00
Michael Shamoon
0d48aea308 Label, visual tweaks 2020-12-09 01:27:35 -08:00
Michael Shamoon
06a3fff2bc Refactor clashing filter variable 2020-12-09 01:27:35 -08:00
Michael Shamoon
c28f19c9cf Quick filter styling 2020-12-09 01:27:35 -08:00
Michael Shamoon
da87542a52 Change advanced to show / hide 2020-12-09 01:27:35 -08:00
Michael Shamoon
23ba3be68f Toggling of items 2020-12-09 01:27:35 -08:00
Michael Shamoon
f3fd0fcf72 Basic tags, correspondents & document type dropdowns 2020-12-09 01:27:35 -08:00
Jonas Winkler
72706a335d Update CONTRIBUTING.md 2020-12-09 01:25:43 -08:00
jonaswinkler
5369e0be03 more bulk edit 2020-12-06 14:39:53 +01:00
jonaswinkler
8699b6931c Merge branch 'dev' into feature-bulk-edit 2020-12-06 02:12:15 +01:00
jonaswinkler
35124023f0 basic support for bulk editing. 2020-11-30 13:59:13 +01:00
jonaswinkler
fd4c9a1758 not sure if this works 2020-11-29 23:00:52 +01:00
jonaswinkler
32186e0de1 added a menu for bulk edits. 2020-11-29 16:33:33 +01:00
204 changed files with 7035 additions and 2011 deletions

View File

@@ -1,13 +1,14 @@
[![Build Status](https://travis-ci.org/jonaswinkler/paperless-ng.svg?branch=master)](https://travis-ci.org/jonaswinkler/paperless-ng)
[![Build Status](https://travis-ci.com/jonaswinkler/paperless-ng.svg?branch=master)](https://travis-ci.com/jonaswinkler/paperless-ng)
[![Documentation Status](https://readthedocs.org/projects/paperless-ng/badge/?version=latest)](https://paperless-ng.readthedocs.io/en/latest/?badge=latest)
[![Gitter](https://badges.gitter.im/paperless-ng/community.svg)](https://gitter.im/paperless-ng/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Docker Hub Pulls](https://img.shields.io/docker/pulls/jonaswinkler/paperless-ng.svg)](https://hub.docker.com/r/jonaswinkler/paperless-ng)
[![Coverage Status](https://coveralls.io/repos/github/jonaswinkler/paperless-ng/badge.svg?branch=master)](https://coveralls.io/github/jonaswinkler/paperless-ng?branch=master)
# Paperless-ng
[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](https://github.com/the-paperless-project/paperless) is an application by Daniel Quinn and contributors 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 +16,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:
@@ -28,20 +31,22 @@ Here's what you get:
# Features
* Performs OCR on your documents, adds selectable text to image only documents and adds tags, correspondents and document types to your documents.
* Paperless stores your documents plain on disk. Filenames and folders are managed by paperless and can be configured freely.
* Single page application front end. Should be pretty snappy. Will be mobile friendly in the future.
* Includes a dashboard that shows basic statistics and has document upload.
* Filtering by tags, correspondents, types, and more.
* Customizable views can be saved and displayed on the dashboard.
* Full text search with auto completion, scored results and query highlighting allows you to quickly find what you need.
* Full text search helps you find what you need.
* Auto completion suggests relevant words from your documents.
* Results are sorted by relevance to your search query.
* Highlighting shows you which parts of the document matched the query.
* Searching for similar documents ("More like this")
* Email processing: Paperless adds documents from your email accounts.
* Configure multiple accounts and filters for each account.
* 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.
If you want to see some screenshots of paperless-ng in action, [some are available in the documentation](https://paperless-ng.readthedocs.io/en/latest/screenshots.html).
@@ -50,34 +55,44 @@ For a complete list of changes from paperless, check out the [changelog](https:/
# Roadmap for 1.0
- Make the front end nice (except mobile).
- Test coverage at 90%.
- Fix whatever bugs I and you find.
## Roadmap for versions beyond 1.0
These are things that I want to add to paperless eventually. They are sorted by priority.
- **Bulk editing**. Add/remove metadata from multiple documents at once.
- **More search.** The search backend is incredibly versatile and customizable. Searching is the most important feature of this project and thus, I want to implement things like:
- Group and limit search results by correspondent, show “more from this” links in the results.
- Ability to search for “Similar documents” in the search results
- **Nested tags**. Organize tags in a hierarchical structure. This will combine the benefits of folders and tags in one coherent system.
- **Localization.** I won't translate paperless into any other languages except English and German, however, I'll add the necessary means so that anyone can translate paperless into their favorite language.
- **An interactive consumer** that shows its progress for documents it processes on the web page.
- With live updates ans websockets. This already works on a dev branch, but requires a lot of new dependencies, which I'm not particular happy about.
- With live updates and websockets. This already works on a dev branch, but requires a lot of new dependencies, which I'm not particularly happy about.
- Notifications when a document was added with buttons to open the new document right away.
- **Arbitrary tag colors**. Allow the selection of any color with a color picker.
- **More file types**. Possibly allow more file types to be processed by paperless, such as office .odt, .doc and .docx documents.
Apart from that, paperless is pretty much feature complete.
## On the chopping block.
- **GnuPG encrypion.** [Here's a note about encryption in paperless](https://paperless-ng.readthedocs.io/en/latest/administration.html#managing-encryption). The gist of it is that I don't see which attacks this implementation protects against. It gives a false sense of security to users who don't care about how it works.
## Wont-do list.
These features will probably never make it into paperless, since paperless is meant to be an easy to use set-and-forget solution.
- **Document versions.** I might consider adding the ability to update a document with a newer version, but that's about it. The kind of documents that get added to paperless usually don't change at all.
- **Workflows.** I don't see a use case for these, yet.
- **Folders.** Tags are superior in just about every way.
- **Apps / extension support.** Again, paperless is meant to be simple.
# Getting started
The recommended way to deploy paperless is docker-compose. Don't clone the repository, grab the latest release to get started instead. The dockerfiles archive contains just the docker files which will pull the image from docker hub. The source archive contains everything you need to build the docker image yourself (i.e. if you want to run on Raspberry Pi).
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
@@ -101,12 +116,13 @@ 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 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.
* [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.
Compatibility with Paperless-ng is unknown.
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.
* [paperless-cli](https://github.com/stgarf/paperless-cli): A golang command line binary to interact with a Paperless instance.
# Important Note

View File

@@ -1,4 +1,4 @@
bind = '127.0.0.1:8000'
bind = ['[::]:8000', 'localhost:8000']
backlog = 2048
workers = 3
worker_class = 'sync'

View File

@@ -15,7 +15,7 @@ services:
POSTGRES_PASSWORD: paperless
webserver:
image: jonaswinkler/paperless-ng:0.9.6
image: jonaswinkler/paperless-ng:0.9.10
restart: always
depends_on:
- db

View File

@@ -5,7 +5,7 @@ services:
restart: always
webserver:
image: jonaswinkler/paperless-ng:0.9.6
image: jonaswinkler/paperless-ng:0.9.10
restart: always
depends_on:
- broker

View File

@@ -9,6 +9,8 @@ RUN apt-get update \
&& apt-get -y --no-install-recommends install \
build-essential \
curl \
file \
fonts-liberation \
ghostscript \
gnupg \
icc-profiles-free \
@@ -20,6 +22,7 @@ RUN apt-get update \
libpq-dev \
libqpdf-dev \
libxml2 \
libxslt1-dev \
optipng \
pngquant \
qpdf \
@@ -62,6 +65,7 @@ RUN sudo -HEu paperless python3 manage.py collectstatic --clear --no-input
VOLUME ["/usr/src/paperless/data", "/usr/src/paperless/media", "/usr/src/paperless/consume", "/usr/src/paperless/export"]
ENTRYPOINT ["/sbin/docker-entrypoint.sh"]
EXPOSE 8000
CMD ["/usr/local/bin/supervisord", "-c", "/etc/supervisord.conf"]
LABEL maintainer="Jonas Winkler <dev@jpwinkler.de>"

View File

@@ -8,7 +8,7 @@ loglevel=info ; log level; default info; others: debug,warn,trace
user=root
[program:gunicorn]
command=gunicorn -c /usr/src/paperless/gunicorn.conf.py -b 0.0.0.0:8000 paperless.wsgi
command=gunicorn -c /usr/src/paperless/gunicorn.conf.py paperless.wsgi
user=paperless
stdout_logfile=/dev/stdout

View File

@@ -5,85 +5,6 @@ Advanced topics
Paperless offers a couple features that automate certain tasks and make your life
easier.
Guesswork
#########
Any document you put into the consumption directory will be consumed, but if
you name the file right, it'll automatically set some values in the database
for you. This is is the logic the consumer follows:
1. Try to find the correspondent, title, and tags in the file name following
the pattern: ``Date - Correspondent - Title - tag,tag,tag.pdf``. Note that
the format of the date is **rigidly defined** as ``YYYYMMDDHHMMSSZ`` or
``YYYYMMDDZ``. The ``Z`` refers "Zulu time" AKA "UTC".
The tags are optional, so the format ``Date - Correspondent - Title.pdf``
works as well.
2. If that doesn't work, we skip the date and try this pattern:
``Correspondent - Title - tag,tag,tag.pdf``.
3. If that doesn't work, we try to find the correspondent and title in the file
name following the pattern: ``Correspondent - Title.pdf``.
4. If that doesn't work, just assume that the name of the file is the title.
So given the above, the following examples would work as you'd expect:
* ``20150314000700Z - Some Company Name - Invoice 2016-01-01 - money,invoices.pdf``
* ``20150314Z - Some Company Name - Invoice 2016-01-01 - money,invoices.pdf``
* ``Some Company Name - Invoice 2016-01-01 - money,invoices.pdf``
* ``Another Company - Letter of Reference.jpg``
* ``Dad's Recipe for Pancakes.png``
These however wouldn't work:
* ``2015-03-14 00:07:00 UTC - Some Company Name, Invoice 2016-01-01, money, invoices.pdf``
* ``2015-03-14 - Some Company Name, Invoice 2016-01-01, money, invoices.pdf``
* ``Some Company Name, Invoice 2016-01-01, money, invoices.pdf``
* ``Another Company- Letter of Reference.jpg``
Do I have to be so strict about naming?
=======================================
Rather than using the strict document naming rules, one can also set the option
``PAPERLESS_FILENAME_DATE_ORDER`` in ``paperless.conf`` to any date order
that is accepted by dateparser_. Doing so will cause ``paperless`` to default
to any date format that is found in the title, instead of a date pulled from
the document's text, without requiring the strict formatting of the document
filename as described above.
.. _dateparser: https://github.com/scrapinghub/dateparser/blob/v0.7.0/docs/usage.rst#settings
.. _advanced-transforming_filenames:
Transforming filenames for parsing
==================================
Some devices can't produce filenames that can be parsed by the default
parser. By configuring the option ``PAPERLESS_FILENAME_PARSE_TRANSFORMS`` in
``paperless.conf`` one can add transformations that are applied to the filename
before it's parsed.
The option contains a list of dictionaries of regular expressions (key:
``pattern``) and replacements (key: ``repl``) in JSON format, which are
applied in order by passing them to ``re.subn``. Transformation stops
after the first match, so at most one transformation is applied. The general
syntax is
.. code:: python
[{"pattern":"pattern1", "repl":"repl1"}, {"pattern":"pattern2", "repl":"repl2"}, ..., {"pattern":"patternN", "repl":"replN"}]
The example below is for a Brother ADS-2400N, a scanner that allows
different names to different hardware buttons (useful for handling
multiple entities in one instance), but insists on adding ``_<count>``
to the filename.
.. code:: python
# Brother profile configuration, support "Name_Date_Count" (the default
# setting) and "Name_Count" (use "Name" as tag and "Count" as title).
PAPERLESS_FILENAME_PARSE_TRANSFORMS=[{"pattern":"^([a-z]+)_(\\d{8})_(\\d{6})_([0-9]+)\\.", "repl":"\\2\\3Z - \\4 - \\1."}, {"pattern":"^([a-z]+)_([0-9]+)\\.", "repl":" - \\2 - \\1."}]
.. _advanced-matching:
Matching tags, correspondents and document types
@@ -263,10 +184,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 +198,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 +219,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 +230,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::

View File

@@ -221,21 +221,16 @@ Each fragment contains a list of strings, and some of them are marked as a highl
[
[
{"text": "This is a sample text with a "},
{"text": "highlighted", "term": 0},
{"text": " word."}
{"text": "This is a sample text with a ", "highlight": false},
{"text": "highlighted", "highlight": true},
{"text": " word.", "highlight": false}
],
[
{"text": "Another", "term": 1},
{"text": " fragment with a highlight."}
{"text": "Another", "highlight": true},
{"text": " fragment with a highlight.", "highlight": false}
]
]
When ``term`` is present within a string, the word within ``text`` should be highlighted.
The term index groups multiple matches together and words with the same index
should get identical highlighting.
A client may use this example to produce the following output:
... This is a sample text with a **highlighted** word. ... **Another** fragment with a highlight. ...

View File

@@ -5,6 +5,123 @@
Changelog
*********
paperless-ng 0.9.10
###################
* Bulk editing
* Thanks to `Michael Shamoon`_, we've got a new interface for the bulk editor.
* There are some configuration options in the settings to alter the behavior.
* Other changes and additions
* The Paperless-ng logo now navigates to the dashboard.
* Filter for documents that don't have any correspondents, types or tags assigned.
* Tags, types and correspondents are now sorted case insensitive.
* Lots of preparation work for localization support.
* Fixes
* Added missing dependencies for Raspberry Pi builds.
* Fixed an issue with plain text file consumption: Thumbnail generation failed due to missing fonts.
* An issue with the search index reporting missing documents after bulk deletes was fixed.
* Issue with the tag selector not clearing input correctly.
* The consumer used to stop working when encountering an incomplete classifier model file.
.. note::
The bulk delete operations did not update the search index. Therefore, documents that you deleted remained in the index and
caused the search to return messages about missing documents when searching. Further bulk operations will properly update
the index.
However, this change is not retroactive: If you used the delete method of the bulk editor, you need to reindex your search index
by :ref:`running the management command document_index with the argument reindex <administration-index>`.
paperless-ng 0.9.9
##################
Christmas release!
* Bulk editing
* Paperless now supports bulk editing.
* The following operations are available: Add and remove correspondents, tags, document types from selected documents, as well as mass-deleting documents.
* We've got a more fancy UI in the works that makes these features more accessible, but that's not quite ready yet.
* Searching
* Paperless now supports searching for similar documents ("More like this") both from the document detail page as well as from individual search results.
* A search score indicates how well a document matches the search query, or how similar a document is to a given reference document.
* Other additions and changes
* Clarification in the UI that the fields "Match" and "Is insensitive" are not relevant for the Auto matching algorithm.
* New select interface for tags, types and correspondents allows filtering. This also improves tag selection. Thanks again to `Michael Shamoon`_!
* Page navigation controls for the document viewer, thanks to `Michael Shamoon`_.
* Layout changes to the small cards document list.
* The dashboard now displays the username (or full name if specified in the admin) on the dashboard.
* Fixes
* An error that caused the document importer to crash was fixed.
* An issue with changes not being possible when ``PAPERLESS_COOKIE_PREFIX`` is used was fixed.
* The date selection filters now allow manual entry of dates.
* Feature Removal
* Most of the guesswork features have been removed. Paperless no longer tries to extract correspondents and tags from file names.
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 +958,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

View File

@@ -152,6 +152,16 @@ PAPERLESS_AUTO_LOGIN_USERNAME=<username>
Defaults to none, which disables this feature.
PAPERLESS_COOKIE_PREFIX=<str>
Specify a prefix that is added to the cookies used by paperless to identify
the currently logged in user. This is useful for when you're running two
instances of paperless on the same host.
After changing this, you will have to login again.
Defaults to ``""``, which does not alter the cookie names.
.. _configuration-ocr:
OCR settings
@@ -390,11 +400,15 @@ PAPERLESS_FILENAME_DATE_ORDER=<format>
Defaults to none, which disables this feature.
PAPERLESS_FILENAME_PARSE_TRANSFORMS
Transforms filenames before they are processed by paperless. See
:ref:`advanced-transforming_filenames` for details.
PAPERLESS_THUMBNAIL_FONT_NAME=<filename>
Paperless creates thumbnails for plain text files by rendering the content
of the file on an image and uses a predefined font for that. This
font can be changed here.
Note that this won't have any effect on already generated thumbnails.
Defaults to ``/usr/share/fonts/liberation/LiberationSerif-Regular.ttf``.
Defaults to none, which disables this feature.
Binaries
########

View File

@@ -78,6 +78,12 @@ that automatically, I'm all ears. For now, you have to grab the latest release
archive from the project page and build the image yourself. The release comes
with the front end already compiled, so you don't have to do this on the Pi.
**Q:** *How do I run this on unRaid?*
**A:** Head over to `<https://github.com/selfhosters/unRAID-CA-templates>`_,
`Uli Fahrer <https://github.com/Tooa>`_ created a container template for that.
I don't exactly know how to use that though, since I don't use unRaid.
**Q:** *How do I run this on my toaster?*
**A:** I honestly don't know! As for all other devices that might be able

View File

@@ -25,6 +25,8 @@ that works right for you based on recommendations from other Paperless users.
+---------+----------------+-----+-----+-----+----------------+
| Fujitsu | `ix500`_ | yes | | yes | `eonist`_ |
+---------+----------------+-----+-----+-----+----------------+
| Epson | `WF-7710DWF`_ | yes | | yes | `Skylinar`_ |
+---------+----------------+-----+-----+-----+----------------+
| Fujitsu | `S1300i`_ | yes | | yes | `jonaswinkler`_|
+---------+----------------+-----+-----+-----+----------------+
@@ -32,7 +34,8 @@ that works right for you based on recommendations from other Paperless users.
.. _MFC-J6930DW: https://www.brother.ca/en/p/MFCJ6930DW
.. _MFC-J5910DW: https://www.brother.co.uk/printers/inkjet-printers/mfcj5910dw
.. _MFC-9142CDN: https://www.brother.co.uk/printers/laser-printers/mfc9140cdn
.. _ix500: https://www.fujitsu.com/global/products/computing/peripheral/scanners/scansnap/ix500/
.. _ix500: http://www.fujitsu.com/us/products/computing/peripheral/scanners/scansnap/ix500/
.. _WF-7710DWF: https://www.epson.de/en/products/printers/inkjet-printers/for-home/workforce-wf-7710dwf
.. _S1300i: https://www.fujitsu.com/global/products/computing/peripheral/scanners/soho/s1300i/
.. _danielquinn: https://github.com/danielquinn
@@ -40,4 +43,5 @@ that works right for you based on recommendations from other Paperless users.
.. _bmsleight: https://github.com/bmsleight
.. _eonist: https://github.com/eonist
.. _REOLDEV: https://github.com/REOLDEV
.. _Skylinar: https://github.com/Skylinar
.. _jonaswinkler: https://github.com/jonaswinkler

View File

@@ -120,6 +120,8 @@ The `bare metal route`_ is more complicated to setup but makes it easier
should you want to contribute some code back. You need to configure and
run the above mentioned components yourself.
.. _setup-docker_route:
Docker Route
============
@@ -219,8 +221,9 @@ writing. Windows is not and will never be supported.
* ``python3-pip``, optionally ``pipenv`` for package installation
* ``python3-dev``
* ``fonts-liberation`` for generating thumbnails for plain text files
* ``imagemagick`` >= 6 for PDF conversion
* ``optipng`` for optimising thumbnails
* ``optipng`` for optimizing thumbnails
* ``gnupg`` for handling encrypted documents
* ``libpoppler-cpp-dev`` for PDF to text conversion
* ``libmagic-dev`` for mime type detection
@@ -240,8 +243,7 @@ writing. Windows is not and will never be supported.
* ``tesseract-ocr`` language packs (``tesseract-ocr-eng``, ``tesseract-ocr-deu``, etc)
You will also need ``build-essential``, ``python3-setuptools`` and ``python3-wheel``
for installing some of the python dependencies. You can remove that
again after installation.
for installing some of the python dependencies.
2. Install ``redis`` >= 5.0 and configure it to start automatically.
@@ -460,6 +462,15 @@ management commands as below.
load data from an old database schema in SQLite into a newer database
schema in PostgreSQL, you will run into trouble.
.. warning::
On some database fields, PostgreSQL enforces predefined limits on maximum
length, whereas SQLite does not. The fields in question are the title of documents
(128 characters), names of document types, tags and correspondents (128 characters),
and filenames (1024 characters). If you have data in these fields that surpasses these
limits, migration to PostgreSQL is not possible and will fail with an error.
1. Stop paperless, if it is running.
2. Tell paperless to use PostgreSQL:

View File

@@ -39,7 +39,7 @@ Operation not permitted
You might see errors such as:
.. code::
.. code:: shell-session
chown: changing ownership of '../export': Operation not permitted
@@ -49,3 +49,29 @@ to these folders. This happens when pointing these directories to NFS shares,
for example.
Ensure that `chown` is possible on these directories.
Classifier error: No training data available
############################################
This indicates that the Auto matching algorithm found no documents to learn from.
This may have two reasons:
* You don't use the Auto matching algorithm: The error can be safely ignored in this case.
* You are using the Auto matching algorithm: The classifier explicitly excludes documents
with Inbox tags. Verify that there are documents in your archive without inbox tags.
The algorithm will only learn from documents not in your inbox.
Permission denied errors in the consumption directory
#####################################################
You might encounter errors such as:
.. code:: shell-session
The following error occured while consuming document.pdf: [Errno 13] Permission denied: '/usr/src/paperless/src/../consume/document.pdf'
This happens when paperless does not have permission to delete files inside the consumption directory.
Ensure that ``USERMAP_UID`` and ``USERMAP_GID`` are set to the user id and group id you use on the host operating system, if these are
different from ``1000``. See :ref:`setup-docker_route`.
Also ensure that you are able to read and write to the consumption directory on the host.

View File

@@ -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 `<https://github.com/qcasey/paperless_share>`_ 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 <https://github.com/bauerj/paperless_app>`_ as well,
which no only has document upload, but also document editing and browsing.
.. _usage-email:
IMAP (Email)

View File

@@ -30,6 +30,7 @@
#PAPERLESS_FORCE_SCRIPT_NAME=
#PAPERLESS_STATIC_URL=/static/
#PAPERLESS_AUTO_LOGIN_USERNAME=
#PAPERLESS_COOKIE_PREFIX=
# OCR settings
@@ -38,7 +39,7 @@
#PAPERLESS_OCR_OUTPUT_TYPE=pdfa
#PAPERLESS_OCR_PAGES=1
#PAPERLESS_OCR_IMAGE_DPI=300
#PAPERLESS_OCR_USER_ARG={}
#PAPERLESS_OCR_USER_ARGS={}
#PAPERLESS_CONVERT_MEMORY_LIMIT=0
#PAPERLESS_CONVERT_TMPDIR=/var/tmp/paperless
@@ -53,6 +54,7 @@
#PAPERLESS_POST_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh
#PAPERLESS_FILENAME_DATE_ORDER=YMD
#PAPERLESS_FILENAME_PARSE_TRANSFORMS=[]
#PAPERLESS_THUMBNAIL_FONT_NAME=
# Binaries

View File

@@ -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:

View File

@@ -26,12 +26,16 @@
"aot": true,
"assets": [
"src/favicon.ico",
"src/assets"
"src/assets",
"src/manifest.webmanifest"
],
"styles": [
"src/styles.scss"
],
"scripts": []
"scripts": [],
"allowedCommonJsDependencies": [
"ng2-pdf-viewer"
]
},
"configurations": {
"production": {
@@ -90,7 +94,8 @@
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/assets"
"src/assets",
"src/manifest.webmanifest"
],
"styles": [
"src/styles.scss"
@@ -127,4 +132,4 @@
}
},
"defaultProject": "paperless-ui"
}
}

1608
src-ui/messages.xlf Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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",
@@ -2215,6 +2223,11 @@
"integrity": "sha512-UV1/ZJMC+HcP902wWdpC43cAcGu0IQk/I5bXjP2aSuCjsk3cE74mDvFrLKga7oDC170ugOAYBwfT4DSQW3akDA==",
"dev": true
},
"@types/pdfjs-dist": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/@types/pdfjs-dist/-/pdfjs-dist-2.1.7.tgz",
"integrity": "sha512-nQIwcPUhkAIyn7x9NS0lR/qxYfd5unRtfGkMjvpgF4Sh28IXftRymaNmFKTTdejDNY25NDGSIyjwj/BRwAPexg=="
},
"@types/q": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz",
@@ -3023,6 +3036,16 @@
"integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==",
"dev": true
},
"bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"dev": true,
"optional": true,
"requires": {
"file-uri-to-path": "1.0.0"
}
},
"blob": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
@@ -5508,6 +5531,13 @@
"schema-utils": "^2.6.5"
}
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"dev": true,
"optional": true
},
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -8208,6 +8238,13 @@
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
"dev": true
},
"nan": {
"version": "2.14.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
"dev": true,
"optional": true
},
"nanomatch": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@@ -8260,6 +8297,23 @@
"moment": "2.18.1"
}
},
"ng2-pdf-viewer": {
"version": "6.3.2",
"resolved": "https://registry.npmjs.org/ng2-pdf-viewer/-/ng2-pdf-viewer-6.3.2.tgz",
"integrity": "sha512-H2tBhDd+Lq6CUzK2g54HsCcZDR2wTn1sDjYqKY3yF0Ydasl2R5ppCKynZBU/zge4EKvmHglJI120FbQMpJKDYQ==",
"requires": {
"@types/pdfjs-dist": "^2.1.4",
"pdfjs-dist": "^2.4.456",
"tslib": "^1.10.0"
},
"dependencies": {
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"ngx-cookie-service": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/ngx-cookie-service/-/ngx-cookie-service-10.1.1.tgz",
@@ -9270,6 +9324,11 @@
"sha.js": "^2.4.8"
}
},
"pdfjs-dist": {
"version": "2.5.207",
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.5.207.tgz",
"integrity": "sha512-xGDUhnCYPfHy+unMXCLCJtlpZaaZ17Ew3WIL0tnSgKFUZXHAPD49GO9xScyszSsQMoutNDgRb+rfBXIaX/lJbw=="
},
"performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
@@ -13228,7 +13287,11 @@
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
"dev": true,
"optional": true
"optional": true,
"requires": {
"bindings": "^1.5.0",
"nan": "^2.12.1"
}
},
"glob-parent": {
"version": "3.1.0",
@@ -13832,7 +13895,11 @@
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
"dev": true,
"optional": true
"optional": true,
"requires": {
"bindings": "^1.5.0",
"nan": "^2.12.1"
}
},
"glob-parent": {
"version": "3.1.0",

View File

@@ -21,8 +21,10 @@
"@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",
"ngx-cookie-service": "^10.1.1",
"ngx-file-drop": "^10.0.0",
"ngx-infinite-scroll": "^9.1.0",

View File

@@ -14,10 +14,9 @@ import { LogsComponent } from './components/manage/logs/logs.component';
import { SettingsComponent } from './components/manage/settings/settings.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { DatePipe } from '@angular/common';
import { SafePipe } from './pipes/safe.pipe';
import { NotFoundComponent } from './components/not-found/not-found.component';
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component';
import { DeleteDialogComponent } from './components/common/delete-dialog/delete-dialog.component';
import { ConfirmDialogComponent } from './components/common/confirm-dialog/confirm-dialog.component';
import { CorrespondentEditDialogComponent } from './components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component';
import { TagEditDialogComponent } from './components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component';
import { DocumentTypeEditDialogComponent } from './components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component';
@@ -27,9 +26,13 @@ import { ResultHighlightComponent } from './components/search/result-highlight/r
import { PageHeaderComponent } from './components/common/page-header/page-header.component';
import { AppFrameComponent } from './components/app-frame/app-frame.component';
import { ToastsComponent } from './components/common/toasts/toasts.component';
import { FilterEditorComponent } from './components/filter-editor/filter-editor.component';
import { FilterEditorComponent } from './components/document-list/filter-editor/filter-editor.component';
import { FilterableDropdownComponent } from './components/common/filterable-dropdown/filterable-dropdown.component';
import { ToggleableDropdownButtonComponent } from './components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component';
import { DateDropdownComponent } from './components/common/date-dropdown/date-dropdown.component';
import { DocumentCardLargeComponent } from './components/document-list/document-card-large/document-card-large.component';
import { DocumentCardSmallComponent } from './components/document-list/document-card-small/document-card-small.component';
import { BulkEditorComponent } from './components/document-list/bulk-editor/bulk-editor.component';
import { NgxFileDropModule } from 'ngx-file-drop';
import { TextComponent } from './components/common/input/text/text.component';
import { SelectComponent } from './components/common/input/select/select.component';
@@ -45,9 +48,15 @@ import { SavedViewWidgetComponent } from './components/dashboard/widgets/saved-v
import { StatisticsWidgetComponent } from './components/dashboard/widgets/statistics-widget/statistics-widget.component';
import { UploadFileWidgetComponent } from './components/dashboard/widgets/upload-file-widget/upload-file-widget.component';
import { WidgetFrameComponent } from './components/dashboard/widgets/widget-frame/widget-frame.component';
import { PdfViewerModule } from 'ng2-pdf-viewer';
import { WelcomeWidgetComponent } from './components/dashboard/widgets/welcome-widget/welcome-widget.component';
import { YesNoPipe } from './pipes/yes-no.pipe';
import { FileSizePipe } from './pipes/file-size.pipe';
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: [
@@ -60,10 +69,9 @@ import { FileSizePipe } from './pipes/file-size.pipe';
DocumentTypeListComponent,
LogsComponent,
SettingsComponent,
SafePipe,
NotFoundComponent,
CorrespondentEditDialogComponent,
DeleteDialogComponent,
ConfirmDialogComponent,
TagEditDialogComponent,
DocumentTypeEditDialogComponent,
TagComponent,
@@ -73,8 +81,12 @@ import { FileSizePipe } from './pipes/file-size.pipe';
AppFrameComponent,
ToastsComponent,
FilterEditorComponent,
FilterableDropdownComponent,
ToggleableDropdownButtonComponent,
DateDropdownComponent,
DocumentCardLargeComponent,
DocumentCardSmallComponent,
BulkEditorComponent,
TextComponent,
SelectComponent,
CheckComponent,
@@ -88,7 +100,11 @@ import { FileSizePipe } from './pipes/file-size.pipe';
WidgetFrameComponent,
WelcomeWidgetComponent,
YesNoPipe,
FileSizePipe
FileSizePipe,
FilterPipe,
DocumentTitlePipe,
MetadataCollapseComponent,
SelectDialogComponent
],
imports: [
BrowserModule,
@@ -98,7 +114,9 @@ import { FileSizePipe } from './pipes/file-size.pipe';
FormsModule,
ReactiveFormsModule,
NgxFileDropModule,
InfiniteScrollModule
InfiniteScrollModule,
PdfViewerModule,
NgSelectModule
],
providers: [
DatePipe,
@@ -106,7 +124,9 @@ import { FileSizePipe } from './pipes/file-size.pipe';
provide: HTTP_INTERCEPTORS,
useClass: CsrfInterceptor,
multi: true
}
},
FilterPipe,
DocumentTitlePipe
],
bootstrap: [AppComponent]
})

View File

@@ -1,158 +1,149 @@
<nav class="navbar navbar-dark sticky-top bg-primary flex-md-nowrap p-0 shadow">
<span class="navbar-brand col-md-3 col-lg-2 mr-0 px-3" href="#">
<a class="navbar-brand col-md-3 col-lg-2 mr-0 px-3" routerLink="/dashboard">
<img src="assets/logo-dark-notext.svg" height="18px" class="mr-2">
Paperless-ng
</span>
<ng-container i18n="app title">Paperless-ng</ng-container>
</a>
<button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-toggle="collapse"
data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation"
(click)="isMenuCollapsed = !isMenuCollapsed">
<span class="navbar-toggler-icon"></span>
</button>
<form (ngSubmit)="search()" class="w-100 m-1">
<input class="form-control form-control-dark" type="text" placeholder="Search" aria-label="Search"
[formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (selectItem)="itemSelected($event)">
<input class="form-control form-control-dark" type="text" placeholder="Search for documents" aria-label="Search"
[formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (selectItem)="itemSelected($event)" i18n-placeholder>
</form>
</nav>
<div class="container-fluid">
<div class="row">
<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse" [ngbCollapse]="isMenuCollapsed">
<div style="position: absolute; bottom: 0; left: 0;" class="text-muted p-1">
{{versionString}}
</div>
<div class="sidebar-sticky pt-3">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" routerLink="dashboard" routerLinkActive="active" (click)="closeMenu()">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#house"/>
</svg>
Dashboard
</svg>&nbsp;<ng-container i18n>Dashboard</ng-container>
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="documents" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" (click)="closeMenu()">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#files"/>
</svg>
Documents
</svg>&nbsp;<ng-container i18n>Documents</ng-container>
</a>
</li>
</ul>
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted" *ngIf='viewConfigService.getSideBarConfigs().length > 0'>
<span>Saved views</span>
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted" *ngIf='savedViewService.sidebarViews.length > 0'>
<ng-container i18n>Saved views</ng-container>
</h6>
<ul class="nav flex-column mb-2">
<li class="nav-item w-100" *ngFor='let config of viewConfigService.getSideBarConfigs()'>
<a class="nav-link text-truncate" routerLink="view/{{config.id}}" routerLinkActive="active" (click)="closeMenu()">
<li class="nav-item w-100" *ngFor="let view of savedViewService.sidebarViews">
<a class="nav-link text-truncate" routerLink="view/{{view.id}}" routerLinkActive="active" (click)="closeMenu()">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#funnel"/>
</svg>
{{config.title}}
</svg>&nbsp;{{view.name}}
</a>
</li>
</ul>
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted" *ngIf='openDocuments.length > 0'>
<span>Open documents</span>
<ng-container i18n>Open documents</ng-container>
</h6>
<ul class="nav flex-column mb-2">
<li class="nav-item w-100" *ngFor='let d of openDocuments'>
<a class="nav-link text-truncate" routerLink="documents/{{d.id}}" routerLinkActive="active" (click)="closeMenu()">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
</svg>
{{d.title}}
</svg>&nbsp;{{d.title | documentTitle}}
</a>
</li>
<li class="nav-item w-100" *ngIf="openDocuments.length > 1">
<a class="nav-link text-truncate" [routerLink]="" (click)="closeAll()">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
Close all
</svg>&nbsp;<ng-container i18n>Close all</ng-container>
</a>
</li>
</ul>
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
<span>Manage</span>
<ng-container i18n>Manage</ng-container>
</h6>
<ul class="nav flex-column mb-2">
<li class="nav-item">
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person"/>
</svg>
Correspondents
</svg>&nbsp;<ng-container i18n>Correspondents</ng-container>
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#tags"/>
</svg>
Tags
</svg>&nbsp;<ng-container i18n>Tags</ng-container>
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#hash"/>
</svg>
Document types
</svg>&nbsp;<ng-container i18n>Document types</ng-container>
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#text-left"/>
</svg>
Logs
</svg>&nbsp;<ng-container i18n>Logs</ng-container>
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
</svg>
Settings
</svg>&nbsp;<ng-container i18n>Settings</ng-container>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="admin/">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#toggles"/>
</svg>
Admin
</svg>&nbsp;<ng-container i18n>Admin</ng-container>
</a>
</li>
</ul>
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
<span>Misc</span>
<ng-container i18n>Misc</ng-container>
</h6>
<ul class="nav flex-column mb-2">
<li class="nav-item">
<a class="nav-link" href="https://paperless-ng.readthedocs.io/en/latest/">
<a class="nav-link" target="_blank" rel="noopener noreferrer" href="https://paperless-ng.readthedocs.io/en/latest/">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#question-circle"/>
</svg>
Documentation
</svg>&nbsp;<ng-container i18n>Documentation</ng-container>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://github.com/jonaswinkler/paperless-ng">
<a class="nav-link" target="_blank" rel="noopener noreferrer" href="https://github.com/jonaswinkler/paperless-ng">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#link"/>
</svg>
GitHub
</svg>&nbsp;<ng-container i18n>GitHub</ng-container>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="accounts/logout/">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#door-open"/>
</svg>
Logout
</svg>&nbsp;<ng-container i18n>Logout</ng-container>
</a>
</li>
</ul>

View File

@@ -5,8 +5,9 @@ import { from, Observable, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, switchMap } from 'rxjs/operators';
import { PaperlessDocument } from 'src/app/data/paperless-document';
import { OpenDocumentsService } from 'src/app/services/open-documents.service';
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
import { SearchService } from 'src/app/services/rest/search.service';
import { SavedViewConfigService } from 'src/app/services/saved-view-config.service';
import { environment } from 'src/environments/environment';
import { DocumentDetailComponent } from '../document-detail/document-detail.component';
@Component({
@@ -21,10 +22,12 @@ export class AppFrameComponent implements OnInit, OnDestroy {
private activatedRoute: ActivatedRoute,
private openDocumentsService: OpenDocumentsService,
private searchService: SearchService,
public viewConfigService: SavedViewConfigService
public savedViewService: SavedViewService
) {
}
versionString = `${environment.appTitle} ${environment.version}`
isMenuCollapsed: boolean = true
closeMenu() {

View File

@@ -5,10 +5,13 @@
</button>
</div>
<div class="modal-body">
<p><b>{{message}}</b></p>
<p *ngIf="message2">{{message2}}</p>
<p *ngIf="messageBold"><b>{{messageBold}}</b></p>
<p *ngIf="message">{{message}}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark" (click)="cancelClicked()">Cancel</button>
<button type="button" class="btn btn-danger" (click)="deleteClicked.emit()">Delete</button>
</div>
<button type="button" class="btn" [class]="btnClass" (click)="confirmClicked.emit()" [disabled]="!confirmButtonEnabled">
{{btnCaption}}
<span *ngIf="!confirmButtonEnabled"> ({{seconds}})</span>
</button>
</div>

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ConfirmDialogComponent } from './confirm-dialog.component';
describe('ConfirmDialogComponent', () => {
let component: ConfirmDialogComponent;
let fixture: ComponentFixture<ConfirmDialogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ConfirmDialogComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ConfirmDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,52 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
@Component({
selector: 'app-confirm-dialog',
templateUrl: './confirm-dialog.component.html',
styleUrls: ['./confirm-dialog.component.scss']
})
export class ConfirmDialogComponent implements OnInit {
constructor(public activeModal: NgbActiveModal) { }
@Output()
public confirmClicked = new EventEmitter()
@Input()
title = $localize`Confirmation`
@Input()
messageBold
@Input()
message
@Input()
btnClass = "btn-primary"
@Input()
btnCaption = $localize`Confirm`
confirmButtonEnabled = true
seconds = 0
delayConfirm(seconds: number) {
this.confirmButtonEnabled = false
this.seconds = seconds
setTimeout(() => {
if (this.seconds <= 1) {
this.confirmButtonEnabled = true
} else {
this.delayConfirm(seconds - 1)
}
}, 1000)
}
ngOnInit(): void {
}
cancelClicked() {
this.activeModal.close()
}
}

View File

@@ -0,0 +1,44 @@
<div class="btn-group" ngbDropdown role="group">
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="dateBefore || dateAfter ? 'btn-primary' : 'btn-outline-primary'">
{{title}}
</button>
<div class="dropdown-menu date-dropdown shadow pt-0" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
<div class="list-group list-group-flush">
<button *ngFor="let qf of quickFilters" class="list-group-item small list-goup list-group-item-action d-flex p-2 pl-3" role="menuitem" (click)="setDateQuickFilter(qf.id)">
{{qf.name}}
</button>
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
<div class="mb-2 d-flex flex-row w-100 justify-content-between small">
<div i18n>After</div>
<a *ngIf="dateAfter" class="btn btn-link p-0 m-0" (click)="clearAfter()">
<svg width="0.8em" height="0.8em" viewBox="0 0 16 16" class="bi bi-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
</svg>
<small i18n>Clear</small>
</a>
</div>
<div class="input-group input-group-sm">
<input type="date" class="form-control" id="date_after" [(ngModel)]="dateAfter" (change)="onChangeDebounce()">
</div>
</div>
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
<div class="mb-2 d-flex flex-row w-100 justify-content-between small">
<div i18n>Before</div>
<a *ngIf="dateBefore" class="btn btn-link p-0 m-0" (click)="clearBefore()">
<svg width="0.8em" height="0.8em" viewBox="0 0 16 16" class="bi bi-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
</svg>
<small i18n>Clear</small>
</a>
</div>
<div class="input-group input-group-sm">
<input type="date" class="form-control" id="date_before" [(ngModel)]="dateBefore" (change)="onChangeDebounce()">
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,7 @@
.date-dropdown {
min-width: 250px;
.btn-link {
line-height: 1;
}
}

View File

@@ -1,20 +1,20 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DeleteDialogComponent } from './delete-dialog.component';
import { DateDropdownComponent } from './date-dropdown.component';
describe('DeleteDialogComponent', () => {
let component: DeleteDialogComponent;
let fixture: ComponentFixture<DeleteDialogComponent>;
describe('DateDropdownComponent', () => {
let component: DateDropdownComponent;
let fixture: ComponentFixture<DateDropdownComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ DeleteDialogComponent ]
declarations: [ DateDropdownComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DeleteDialogComponent);
fixture = TestBed.createComponent(DateDropdownComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@@ -0,0 +1,111 @@
import { formatDate } from '@angular/common';
import { Component, EventEmitter, Input, Output, OnInit, OnDestroy } from '@angular/core';
import { Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
export interface DateSelection {
before?: string
after?: string
}
const LAST_7_DAYS = 0
const LAST_MONTH = 1
const LAST_3_MONTHS = 2
const LAST_YEAR = 3
@Component({
selector: 'app-date-dropdown',
templateUrl: './date-dropdown.component.html',
styleUrls: ['./date-dropdown.component.scss']
})
export class DateDropdownComponent implements OnInit, OnDestroy {
quickFilters = [
{id: LAST_7_DAYS, name: $localize`Last 7 days`},
{id: LAST_MONTH, name: $localize`Last month`},
{id: LAST_3_MONTHS, name: $localize`Last 3 months`},
{id: LAST_YEAR, name: $localize`Last year`}
]
@Input()
dateBefore: string
@Output()
dateBeforeChange = new EventEmitter<string>()
@Input()
dateAfter: string
@Output()
dateAfterChange = new EventEmitter<string>()
@Input()
title: string
@Output()
datesSet = new EventEmitter<DateSelection>()
private datesSetDebounce$ = new Subject()
private sub: Subscription
ngOnInit() {
this.sub = this.datesSetDebounce$.pipe(
debounceTime(400)
).subscribe(() => {
this.onChange()
})
}
ngOnDestroy() {
if (this.sub) {
this.sub.unsubscribe()
}
}
setDateQuickFilter(qf: number) {
this.dateBefore = null
let date = new Date()
switch (qf) {
case LAST_7_DAYS:
date.setDate(date.getDate() - 7)
break;
case LAST_MONTH:
date.setMonth(date.getMonth() - 1)
break;
case LAST_3_MONTHS:
date.setMonth(date.getMonth() - 3)
break
case LAST_YEAR:
date.setFullYear(date.getFullYear() - 1)
break
}
this.dateAfter = formatDate(date, 'yyyy-MM-dd', "en-us", "UTC")
this.onChange()
}
onChange() {
this.dateAfterChange.emit(this.dateAfter)
this.dateBeforeChange.emit(this.dateBefore)
this.datesSet.emit({after: this.dateAfter, before: this.dateBefore})
}
onChangeDebounce() {
this.datesSetDebounce$.next({after: this.dateAfter, before: this.dateBefore})
}
clearBefore() {
this.dateBefore = null
this.onChange()
}
clearAfter() {
this.dateAfter = null
this.onChange()
}
}

View File

@@ -1,31 +0,0 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
@Component({
selector: 'app-delete-dialog',
templateUrl: './delete-dialog.component.html',
styleUrls: ['./delete-dialog.component.scss']
})
export class DeleteDialogComponent implements OnInit {
constructor(public activeModal: NgbActiveModal) { }
@Output()
public deleteClicked = new EventEmitter()
@Input()
title = "Delete confirmation"
@Input()
message = "Do you really want to delete this?"
@Input()
message2
ngOnInit(): void {
}
cancelClicked() {
this.activeModal.close()
}
}

View File

@@ -5,7 +5,7 @@ import { Observable } from 'rxjs';
import { MATCHING_ALGORITHMS } from 'src/app/data/matching-model';
import { ObjectWithId } from 'src/app/data/object-with-id';
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service';
import { Toast, ToastService } from 'src/app/services/toast.service';
import { ToastService } from 'src/app/services/toast.service';
@Directive()
export abstract class EditDialogComponent<T extends ObjectWithId> implements OnInit {
@@ -13,8 +13,7 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI
constructor(
private service: AbstractPaperlessService<T>,
private activeModal: NgbActiveModal,
private toastService: ToastService,
private entityName: string) { }
private toastService: ToastService) { }
@Input()
dialogMode: string = 'create'
@@ -35,12 +34,24 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI
}
}
getCreateTitle() {
return $localize`Create new item`
}
getEditTitle() {
return $localize`Edit item`
}
getSaveErrorMessage(error: string) {
return $localize`Could not save element: ${error}`
}
getTitle() {
switch (this.dialogMode) {
case 'create':
return "Create new " + this.entityName
return this.getCreateTitle()
case 'edit':
return "Edit " + this.entityName
return this.getEditTitle()
default:
break;
}
@@ -66,7 +77,7 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI
this.activeModal.close()
this.success.emit(result)
}, error => {
this.toastService.showToast(Toast.makeError(`Could not save ${this.entityName}: ${error.error.name}`))
this.toastService.showError(this.getSaveErrorMessage(error.error.name))
})
}

View File

@@ -0,0 +1,35 @@
<div class="btn-group" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown">
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'">
<div class="d-none d-md-inline">{{title}}</div>
<div class="d-inline-block d-md-none">
<svg class="toolbaricon" fill="currentColor">
<use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
</svg>
</div>
<ng-container *ngIf="!editing && selectionModel.selectionSize() > 0">
<div class="badge bg-secondary text-light rounded-pill badge-corner">
{{selectionModel.selectionSize()}}
</div>
</ng-container>
</button>
<div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
<div class="list-group list-group-flush">
<div class="list-group-item">
<div class="input-group input-group-sm">
<input class="form-control" type="text" [(ngModel)]="filterText" [placeholder]="filterPlaceholder" (keyup.enter)="listFilterEnter()" #listFilterTextInput>
</div>
</div>
<div *ngIf="selectionModel.items" class="items">
<ng-container *ngFor="let item of (editing ? selectionModel.itemsSorted : selectionModel.items) | filter: filterText">
<app-toggleable-dropdown-button *ngIf="allowSelectNone || item.id" [item]="item" [state]="selectionModel.get(item.id)" (toggle)="selectionModel.toggle(item.id)"></app-toggleable-dropdown-button>
</ng-container>
</div>
<button *ngIf="editing" class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!selectionModel.isDirty()">
<small class="ml-1" [ngClass]="{'font-weight-bold': selectionModel.isDirty()}" i18n>Apply</small>
<svg width="1.5em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#arrow-right" />
</svg>
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,14 @@
.badge-corner {
position: absolute;
top: -8px;
right: -8px;
}
.dropdown-menu {
min-width: 250px;
.items {
max-height: 400px;
overflow-y: scroll;
}
}

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FilterableDropodownComponent } from './filterable-dropdown.component';
describe('FilterableDropodownComponent', () => {
let component: FilterableDropodownComponent;
let fixture: ComponentFixture<FilterableDropodownComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ FilterableDropodownComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FilterableDropodownComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,267 @@
import { Component, EventEmitter, Input, Output, ElementRef, ViewChild } from '@angular/core';
import { FilterPipe } from 'src/app/pipes/filter.pipe';
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
import { ToggleableItemState } from './toggleable-dropdown-button/toggleable-dropdown-button.component';
import { MatchingModel } from 'src/app/data/matching-model';
import { Subject } from 'rxjs';
export interface ChangedItems {
itemsToAdd: MatchingModel[],
itemsToRemove: MatchingModel[]
}
export class FilterableDropdownSelectionModel {
changed = new Subject<FilterableDropdownSelectionModel>()
multiple = false
items: MatchingModel[] = []
get itemsSorted(): MatchingModel[] {
return this.items.sort((a,b) => {
if (this.getNonTemporary(a.id) == ToggleableItemState.NotSelected && this.getNonTemporary(b.id) != ToggleableItemState.NotSelected) {
return 1
} else if (this.getNonTemporary(a.id) != ToggleableItemState.NotSelected && this.getNonTemporary(b.id) == ToggleableItemState.NotSelected) {
return -1
} else {
return a.name.localeCompare(b.name)
}
})
}
private selectionStates = new Map<number, ToggleableItemState>()
private temporarySelectionStates = new Map<number, ToggleableItemState>()
getSelectedItems() {
return this.items.filter(i => this.temporarySelectionStates.get(i.id) == ToggleableItemState.Selected)
}
set(id: number, state: ToggleableItemState, fireEvent = true) {
if (state == ToggleableItemState.NotSelected) {
this.temporarySelectionStates.delete(id)
} else {
this.temporarySelectionStates.set(id, state)
}
if (fireEvent) {
this.changed.next(this)
}
}
toggle(id: number, fireEvent = true) {
let state = this.temporarySelectionStates.get(id)
if (state == null || state != ToggleableItemState.Selected) {
this.temporarySelectionStates.set(id, ToggleableItemState.Selected)
} else if (state == ToggleableItemState.Selected) {
this.temporarySelectionStates.delete(id)
}
if (!this.multiple) {
for (let key of this.temporarySelectionStates.keys()) {
if (key != id) {
this.temporarySelectionStates.delete(key)
}
}
}
if (!id) {
for (let key of this.temporarySelectionStates.keys()) {
if (key) {
this.temporarySelectionStates.delete(key)
}
}
} else {
this.temporarySelectionStates.delete(null)
}
if (fireEvent) {
this.changed.next(this)
}
}
private getNonTemporary(id: number) {
return this.selectionStates.get(id) || ToggleableItemState.NotSelected
}
get(id: number) {
return this.temporarySelectionStates.get(id) || ToggleableItemState.NotSelected
}
selectionSize() {
return this.getSelectedItems().length
}
clear(fireEvent = true) {
this.temporarySelectionStates.clear()
if (fireEvent) {
this.changed.next(this)
}
}
isDirty() {
if (!Array.from(this.temporarySelectionStates.keys()).every(id => this.temporarySelectionStates.get(id) == this.selectionStates.get(id))) {
return true
} else if (!Array.from(this.selectionStates.keys()).every(id => this.selectionStates.get(id) == this.temporarySelectionStates.get(id))) {
return true
} else {
return false
}
}
isNoneSelected() {
return this.selectionSize() == 1 && this.get(null) == ToggleableItemState.Selected
}
init(map) {
this.temporarySelectionStates = map
this.apply()
}
apply() {
this.selectionStates.clear()
this.temporarySelectionStates.forEach((value, key) => {
this.selectionStates.set(key, value)
})
}
reset() {
this.temporarySelectionStates.clear()
this.selectionStates.forEach((value, key) => {
this.temporarySelectionStates.set(key, value)
})
}
diff(): ChangedItems {
return {
itemsToAdd: this.items.filter(item => this.temporarySelectionStates.get(item.id) == ToggleableItemState.Selected && this.selectionStates.get(item.id) != ToggleableItemState.Selected),
itemsToRemove: this.items.filter(item => !this.temporarySelectionStates.has(item.id) && this.selectionStates.has(item.id)),
}
}
}
@Component({
selector: 'app-filterable-dropdown',
templateUrl: './filterable-dropdown.component.html',
styleUrls: ['./filterable-dropdown.component.scss']
})
export class FilterableDropdownComponent {
@ViewChild('listFilterTextInput') listFilterTextInput: ElementRef
@ViewChild('dropdown') dropdown: NgbDropdown
filterText: string
@Input()
set items(items: MatchingModel[]) {
if (items) {
this._selectionModel.items = Array.from(items)
this._selectionModel.items.unshift({
name: $localize`:Filter drop down element to filter for documents with no correspondent/type/tag assigned:Not assigned`,
id: null
})
}
}
get items(): MatchingModel[] {
return this._selectionModel.items
}
_selectionModel = new FilterableDropdownSelectionModel()
@Input()
set selectionModel(model: FilterableDropdownSelectionModel) {
if (this.selectionModel) {
this.selectionModel.changed.complete()
model.items = this.selectionModel.items
model.multiple = this.selectionModel.multiple
}
model.changed.subscribe(updatedModel => {
this.selectionModelChange.next(updatedModel)
})
this._selectionModel = model
}
get selectionModel(): FilterableDropdownSelectionModel {
return this._selectionModel
}
@Output()
selectionModelChange = new EventEmitter<FilterableDropdownSelectionModel>()
@Input()
set multiple(value: boolean) {
this.selectionModel.multiple = value
}
get multiple() {
return this.selectionModel.multiple
}
@Input()
title: string
@Input()
filterPlaceholder: string = ""
@Input()
icon: string
@Input()
allowSelectNone: boolean = false
@Input()
editing = false
@Input()
applyOnClose = false
@Output()
apply = new EventEmitter<ChangedItems>()
@Output()
open = new EventEmitter()
constructor(private filterPipe: FilterPipe) {
this.selectionModel = new FilterableDropdownSelectionModel()
}
applyClicked() {
if (this.selectionModel.isDirty()) {
this.dropdown.close()
if (!this.applyOnClose) {
this.apply.emit(this.selectionModel.diff())
}
}
}
dropdownOpenChange(open: boolean): void {
if (open) {
setTimeout(() => {
this.listFilterTextInput.nativeElement.focus();
}, 0)
if (this.editing) {
this.selectionModel.reset()
}
this.open.next()
} else {
this.filterText = ''
if (this.applyOnClose && this.selectionModel.isDirty()) {
this.apply.emit(this.selectionModel.diff())
}
}
}
listFilterEnter(): void {
let filtered = this.filterPipe.transform(this.items, this.filterText)
if (filtered.length == 1) {
this.selectionModel.toggle(filtered[0].id)
if (this.editing) {
this.applyClicked()
} else {
this.dropdown.close()
}
}
}
}

View File

@@ -0,0 +1,20 @@
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-left-0 border-right-0 border-bottom" role="menuitem" (click)="toggleItem()">
<div class="selected-icon mr-1">
<ng-container *ngIf="isChecked()">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16">
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
</svg>
</ng-container>
<ng-container *ngIf="isPartiallyChecked()">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-dash" viewBox="0 0 16 16">
<path d="M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8z"/>
</svg>
</ng-container>
</div>
<div class="mr-1">
<app-tag *ngIf="isTag; else displayName" [tag]="item" [clickable]="true" linkTitle="Filter by tag"></app-tag>
<ng-template #displayName><small>{{item.name}}</small></ng-template>
</div>
<div class="badge badge-light rounded-pill ml-auto mr-1">{{item.document_count}}</div>
</button>

View File

@@ -0,0 +1,4 @@
.selected-icon {
min-width: 1em;
min-height: 1em;
}

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ToggleableDropdownButtonComponent } from './toggleable-dropdown-button.component';
describe('ToggleableDropdownButtonComponent', () => {
let component: ToggleableDropdownButtonComponent;
let fixture: ComponentFixture<ToggleableDropdownButtonComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ToggleableDropdownButtonComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ToggleableDropdownButtonComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,51 @@
import { Component, EventEmitter, Input, Output, OnInit } from '@angular/core';
import { MatchingModel } from 'src/app/data/matching-model';
export interface ToggleableItem {
item: MatchingModel,
state: ToggleableItemState,
count: number
}
export enum ToggleableItemState {
NotSelected = 0,
Selected = 1,
PartiallySelected = 2
}
@Component({
selector: 'app-toggleable-dropdown-button',
templateUrl: './toggleable-dropdown-button.component.html',
styleUrls: ['./toggleable-dropdown-button.component.scss']
})
export class ToggleableDropdownButtonComponent {
@Input()
item: MatchingModel
@Input()
state: ToggleableItemState
@Input()
count: number
@Output()
toggle = new EventEmitter()
get isTag(): boolean {
return 'is_inbox_tag' in this.item
}
toggleItem(): void {
this.toggle.emit()
}
isChecked() {
return this.state == ToggleableItemState.Selected
}
isPartiallyChecked() {
return this.state == ToggleableItemState.PartiallySelected
}
}

View File

@@ -1,5 +1,5 @@
import { Component, Directive, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Directive, Input, OnInit } from '@angular/core';
import { ControlValueAccessor } from '@angular/forms';
import { v4 as uuidv4 } from 'uuid';
@Directive()

View File

@@ -1,7 +1,6 @@
import { formatDate } from '@angular/common';
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { AbstractInputComponent } from '../abstract-input';
@Component({
providers: [{

View File

@@ -1,11 +1,16 @@
<div class="form-group">
<div class="form-group paperless-input-select">
<label [for]="inputId">{{title}}</label>
<div [class.input-group]="showPlusButton()">
<select class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()"
[disabled]="disabled" [style.color]="textColor" [style.background]="backgroundColor">
<option *ngIf="allowNull" [ngValue]="null" class="form-control">---</option>
<option *ngFor="let i of items" [ngValue]="i.id" class="form-control">{{i.name}}</option>
</select>
<ng-select name="inputId" [(ngModel)]="value"
[disabled]="disabled"
[style.color]="textColor"
[style.background]="backgroundColor"
[clearable]="allowNull"
(change)="onChange(value)"
(blur)="onTouched()">
<ng-option *ngFor="let i of items" [value]="i.id">{{i.name}}</ng-option>
</ng-select>
<div *ngIf="showPlusButton()" class="input-group-append">
<button class="btn btn-outline-secondary" type="button" (click)="createNew.emit()">
<svg class="buttonicon" fill="currentColor">
@@ -15,4 +20,4 @@
</div>
</div>
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
</div>
</div>

View File

@@ -0,0 +1 @@
// styles for ng-select child are in styles.scss

View File

@@ -1,4 +1,4 @@
import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core';
import { Component, EventEmitter, forwardRef, Input, Output } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { AbstractInputComponent } from '../abstract-input';

View File

@@ -1,30 +1,43 @@
<div class="form-group">
<label for="exampleFormControlTextarea1">Tags</label>
<div class="form-group paperless-input-select paperless-input-tags">
<label for="tags">Tags</label>
<div class="input-group">
<div class="form-control tags-form-control" id="tags">
<app-tag class="mr-2" *ngFor="let id of displayValue" [tag]="getTag(id)" (click)="removeTag(id)"></app-tag>
</div>
<div class="input-group flex-nowrap">
<ng-select name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="displayValue"
[multiple]="true"
[closeOnSelect]="false"
[clearSearchOnAdd]="true"
[disabled]="disabled"
[hideSelected]="true"
(change)="ngSelectChange()">
<div class="input-group-append" ngbDropdown placement="top-right">
<button class="btn btn-outline-secondary" type="button" ngbDropdownToggle></button>
<div ngbDropdownMenu class="scrollable-menu shadow">
<button type="button" *ngFor="let tag of tags" ngbDropdownItem (click)="addTag(tag.id)">
<app-tag [tag]="tag"></app-tag>
</button>
</div>
</div>
<ng-template ng-label-tmp let-item="item">
<span class="tag-wrap tag-wrap-delete" (click)="removeTag(item.id)">
<svg width="1.2em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
<app-tag style="background-color: none;" [tag]="getTag(item.id)"></app-tag>
</span>
</ng-template>
<ng-template ng-option-tmp let-item="item" let-index="index" let-search="searchTerm">
<div class="tag-wrap">
<div class="selected-icon d-inline-block mr-1">
<svg *ngIf="displayValue.includes(item.id)" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
</div>
<app-tag class="mr-2" [tag]="getTag(item.id)"></app-tag>
</div>
</ng-template>
</ng-select>
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" (click)="createTag()">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#plus" />
</svg>
</button>
</div>
</div>
<small class="form-text text-muted" *ngIf="hint">{{hint}}</small>
</div>
</div>

View File

@@ -1,10 +1,12 @@
.tags-form-control {
height: auto;
.selected-icon {
min-width: 1em;
min-height: 1em;
}
.tag-wrap {
font-size: 1rem;
}
.scrollable-menu {
height: auto;
max-height: 300px;
overflow-x: hidden;
}
.tag-wrap-delete {
cursor: pointer;
}

View File

@@ -1,8 +1,6 @@
import { ThrowStmt } from '@angular/compiler';
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { Observable } from 'rxjs';
import { TagEditDialogComponent } from 'src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component';
import { PaperlessTag } from 'src/app/data/paperless-tag';
import { TagService } from 'src/app/services/rest/tag.service';
@@ -23,7 +21,7 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
onChange = (newValue: number[]) => {};
onTouched = () => {};
writeValue(newValue: number[]): void {
@@ -68,29 +66,28 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
removeTag(id) {
let index = this.displayValue.indexOf(id)
if (index > -1) {
this.displayValue.splice(index, 1)
let oldValue = this.displayValue
oldValue.splice(index, 1)
this.displayValue = [...oldValue]
this.onChange(this.displayValue)
}
}
addTag(id) {
let index = this.displayValue.indexOf(id)
if (index == -1) {
this.displayValue.push(id)
this.onChange(this.displayValue)
}
}
createTag() {
var modal = this.modalService.open(TagEditDialogComponent, {backdrop: 'static'})
modal.componentInstance.dialogMode = 'create'
modal.componentInstance.success.subscribe(newTag => {
this.tagService.listAll().subscribe(tags => {
this.tags = tags.results
this.addTag(newTag.id)
this.displayValue = [...this.displayValue, newTag.id]
this.onChange(this.displayValue)
})
})
}
ngSelectChange() {
this.value = this.displayValue
this.onChange(this.displayValue)
}
}

View File

@@ -1,21 +1,29 @@
import { Component, Input, OnInit } from '@angular/core';
import { Component, Input } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { environment } from 'src/environments/environment';
@Component({
selector: 'app-page-header',
templateUrl: './page-header.component.html',
styleUrls: ['./page-header.component.scss']
})
export class PageHeaderComponent implements OnInit {
export class PageHeaderComponent {
constructor() { }
constructor(private titleService: Title) { }
_title = ""
@Input()
title: string = ""
set title(title: string) {
this._title = title
this.titleService.setTitle(`${this.title} - ${environment.appTitle}`)
}
get title() {
return this._title
}
@Input()
subTitle: string = ""
ngOnInit(): void {
}
}

View File

@@ -0,0 +1,15 @@
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
<button type="button" class="close" aria-label="Close" (click)="cancelClicked()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<app-input-select [items]="objects" [title]="message" [(ngModel)]="selected"></app-input-select>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark" (click)="cancelClicked()" i18n>Cancel</button>
<button type="button" class="btn btn-primary" (click)="selectClicked.emit(selected)" i18n>Select</button>
</div>

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SelectDialogComponent } from './select-dialog.component';
describe('SelectDialogComponent', () => {
let component: SelectDialogComponent;
let fixture: ComponentFixture<SelectDialogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ SelectDialogComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SelectDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,34 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { ObjectWithId } from 'src/app/data/object-with-id';
@Component({
selector: 'app-select-dialog',
templateUrl: './select-dialog.component.html',
styleUrls: ['./select-dialog.component.scss']
})
export class SelectDialogComponent implements OnInit {
constructor(public activeModal: NgbActiveModal) { }
@Output()
public selectClicked = new EventEmitter()
@Input()
title = $localize`Select`
@Input()
message = $localize`Please select an object`
@Input()
objects: ObjectWithId[] = []
selected: number
ngOnInit(): void {
}
cancelClicked() {
this.activeModal.close()
}
}

View File

@@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Component, Input, OnInit } from '@angular/core';
import { TAG_COLOURS, PaperlessTag } from 'src/app/data/paperless-tag';
@Component({

View File

@@ -1,4 +1,4 @@
<app-page-header title="Dashboard" subTitle="Welcome to paperless-ng!">
<app-page-header title="Dashboard" [subTitle]="subtitle" i18n-title>
<img src="assets/logo.svg" height="80" class="m-2 d-none d-md-block">
</app-page-header>

View File

@@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { SavedViewConfigService } from 'src/app/services/saved-view-config.service';
import { environment } from 'src/environments/environment';
import { Meta } from '@angular/platform-browser';
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
@Component({
@@ -12,15 +12,36 @@ import { environment } from 'src/environments/environment';
export class DashboardComponent implements OnInit {
constructor(
public savedViewConfigService: SavedViewConfigService,
private titleService: Title) { }
private savedViewService: SavedViewService,
private meta: Meta
) { }
get displayName() {
let tagFullName = this.meta.getTag('name=full_name')
let tagUsername = this.meta.getTag('name=username')
if (tagFullName && tagFullName.content) {
return tagFullName.content
} else if (tagUsername && tagUsername.content) {
return tagUsername.content
} else {
return null
}
}
savedViews = []
get subtitle() {
if (this.displayName) {
return $localize`Hello ${this.displayName}, welcome to Paperless-ng!`
} else {
return $localize`Welcome to Paperless-ng!`
}
}
savedViews: PaperlessSavedView[] = []
ngOnInit(): void {
this.savedViews = this.savedViewConfigService.getDashboardConfigs()
this.titleService.setTitle(`Dashboard - ${environment.appTitle}`)
this.savedViewService.listAll().subscribe(results => {
this.savedViews = results.results.filter(savedView => savedView.show_on_dashboard)
})
}
}

View File

@@ -1,19 +1,19 @@
<app-widget-frame [title]="savedView.title">
<app-widget-frame [title]="savedView.name">
<a header-buttons [routerLink]="" (click)="showAll()">Show all</a>
<a header-buttons [routerLink]="" (click)="showAll()" i18n>Show all</a>
<table content class="table table-sm table-hover table-borderless">
<thead>
<tr>
<th>Created</th>
<th scope="col">Title</th>
<th i18n>Created</th>
<th scope="col" i18n>Title</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let doc of documents" routerLink="/documents/{{doc.id}}">
<td>{{doc.created | date}}</td>
<td>{{doc.title}}<app-tag [tag]="t" *ngFor="let t of doc.tags$ | async" class="ml-1"></app-tag>
<td>{{doc.title | documentTitle}}<app-tag [tag]="t" *ngFor="let t of doc.tags$ | async" class="ml-1"></app-tag>
</tr>
</tbody>
</table>

View File

@@ -1,7 +1,7 @@
import { Component, Input, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { PaperlessDocument } from 'src/app/data/paperless-document';
import { SavedViewConfig } from 'src/app/data/saved-view-config';
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
import { DocumentService } from 'src/app/services/rest/document.service';
@@ -18,18 +18,18 @@ export class SavedViewWidgetComponent implements OnInit {
private list: DocumentListViewService) { }
@Input()
savedView: SavedViewConfig
savedView: PaperlessSavedView
documents: PaperlessDocument[] = []
ngOnInit(): void {
this.documentService.list(1,10,this.savedView.sortField,this.savedView.sortDirection,this.savedView.filterRules).subscribe(result => {
this.documentService.listFiltered(1,10,this.savedView.sort_field, this.savedView.sort_reverse, this.savedView.filter_rules).subscribe(result => {
this.documents = result.results
})
}
showAll() {
if (this.savedView.showInSideBar) {
if (this.savedView.show_in_sidebar) {
this.router.navigate(['view', this.savedView.id])
} else {
this.list.load(this.savedView)

View File

@@ -1,6 +1,6 @@
<app-widget-frame title="Statistics">
<app-widget-frame title="Statistics" i18n-title>
<ng-container content>
<p class="card-text">Documents in inbox: {{statistics.documents_inbox}}</p>
<p class="card-text">Total documents: {{statistics.documents_total}}</p>
<p class="card-text" i18n>Documents in inbox: {{statistics.documents_inbox}}</p>
<p class="card-text" i18n>Total documents: {{statistics.documents_total}}</p>
</ng-container>
</app-widget-frame>

View File

@@ -1,16 +1,16 @@
<app-widget-frame title="Upload new documents">
<app-widget-frame title="Upload new documents" i18n-title>
<div content>
<form>
<ngx-file-drop dropZoneLabel="Drop documents here or" (onFileDrop)="dropped($event)"
<ngx-file-drop dropZoneLabel="Drop documents here or" browseBtnLabel="Browse files" (onFileDrop)="dropped($event)"
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" dropZoneClassName="bg-light card"
multiple="true" contentClassName="justify-content-center d-flex align-items-center p-5" [showBrowseBtn]=true
browseBtnClassName="btn btn-sm btn-outline-primary ml-2">
browseBtnClassName="btn btn-sm btn-outline-primary ml-2" i18n-dropZoneLabel i18n-browseBtnLabel>
</ngx-file-drop>
</form>
<div *ngIf="uploadVisible" class="mt-3">
<p>Uploading {{uploadStatus.length}} file(s)</p>
<p i18n>Uploading {uploadStatus.length, plural, =1 {file} =other {{{uploadStatus.length}} files}}...</p>
<ngb-progressbar [value]="loadedSum" [max]="totalSum" [striped]="true" [animated]="uploadStatus.length > 0">
</ngb-progressbar>
</div>

View File

@@ -2,7 +2,7 @@ import { HttpEventType } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop';
import { DocumentService } from 'src/app/services/rest/document.service';
import { Toast, ToastService } from 'src/app/services/toast.service';
import { ToastService } from 'src/app/services/toast.service';
interface UploadStatus {
@@ -60,7 +60,7 @@ export class UploadFileWidgetComponent implements OnInit {
} else if (event.type == HttpEventType.Response) {
this.uploadStatus.splice(this.uploadStatus.indexOf(uploadStatusObject), 1)
this.completedFiles += 1
this.toastService.showToast(Toast.make("Information", "The document has been uploaded and will be processed by the consumer shortly."))
this.toastService.showInfo($localize`The document has been uploaded and will be processed by the consumer shortly.`)
}
}, error => {
@@ -68,11 +68,11 @@ export class UploadFileWidgetComponent implements OnInit {
this.completedFiles += 1
switch (error.status) {
case 400: {
this.toastService.showToast(Toast.makeError(`There was an error while uploading the document: ${error.error.document}`))
this.toastService.showInfo($localize`There was an error while uploading the document: ${error.error.document}`)
break;
}
default: {
this.toastService.showToast(Toast.makeError("An error has occurred while uploading the document. Sorry!"))
this.toastService.showInfo($localize`An error has occurred while uploading the document. Sorry!`)
break;
}
}

View File

@@ -1,16 +1,16 @@
<app-widget-frame title="First steps">
<app-widget-frame title="First steps" i18n-title>
<ng-container content>
<img src="assets/save-filter.png" class="float-right">
<p>Paperless is running! :)</p>
<p>You can start uploading documents by dropping them in the file upload box to the right or by dropping them in the configured consumption folder and they'll start showing up in the documents list.
After you've added some metadata to your documents, use the filtering mechanisms of paperless to create custom views (such as 'Recently added', 'Tagged TODO') and have them displayed on the dashboard instead of this message.</p>
<p>Paperless offers some more features that try to make your life easier, such as:</p>
<p i18n>Paperless is running! :)</p>
<p i18n>You can start uploading documents by dropping them in the file upload box to the right or by dropping them in the configured consumption folder and they'll start showing up in the documents list.
After you've added some metadata to your documents, use the filtering mechanisms of paperless to create custom views (such as 'Recently added', 'Tagged TODO') and they will appear on the dashboard instead of this message.</p>
<p i18n>Paperless offers some more features that try to make your life easier:</p>
<ul>
<li>Once you've got a couple documents in paperless and added metadata to them, paperless can assign that metadata to new documents automatically.</li>
<li>You can configure paperless to read your mails and add documents from attached files.</li>
<li i18n>Once you've got a couple documents in paperless and added metadata to them, paperless can assign that metadata to new documents automatically.</li>
<li i18n>You can configure paperless to read your mails and add documents from attached files.</li>
</ul>
<p>Consult the documentation on how to use these features. The section on basic usage also has some information on how to use paperless in general.</p>
<p i18n>Consult the documentation on how to use these features. The section on basic usage also has some information on how to use paperless in general.</p>
</ng-container>
</app-widget-frame>

View File

@@ -1,9 +1,18 @@
<app-page-header [(title)]="title">
<div class="input-group input-group-sm mr-5" *ngIf="getContentType() == 'application/pdf'">
<div class="input-group-prepend">
<div class="input-group-text" i18n>Page</div>
</div>
<input class="form-control flex-grow-0 w-auto" type="number" min="1" [max]="previewNumPages" [(ngModel)]="previewCurrentPage" />
<div class="input-group-append">
<div class="input-group-text" i18n>of {{previewNumPages}}</div>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-danger mr-2" (click)="delete()">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg>
<span class="d-none d-lg-inline"> Delete</span>
</svg>&nbsp;<span class="d-none d-lg-inline" i18n>Delete</span>
</button>
<div class="btn-group mr-2">
@@ -11,57 +20,60 @@
<a [href]="downloadUrl" class="btn btn-sm btn-outline-primary">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#download" />
</svg>
<span class="d-none d-lg-inline"> Download</span>
</svg>&nbsp;<span class="d-none d-lg-inline" i18n>Download</span>
</a>
<div class="btn-group" ngbDropdown role="group" *ngIf="metadata?.has_archive_version">
<button class="btn btn-sm btn-outline-primary dropdown-toggle-split" ngbDropdownToggle></button>
<div class="dropdown-menu shadow" ngbDropdownMenu>
<a ngbDropdownItem [href]="downloadOriginalUrl">Download original</a>
<a ngbDropdownItem [href]="downloadOriginalUrl" i18n>Download original</a>
</div>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-primary mr-2" (click)="moreLike()">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#three-dots" />
</svg>&nbsp;<span class="d-none d-lg-inline" i18n>More like this</span>
</button>
<button type="button" class="btn btn-sm btn-outline-primary" (click)="close()">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x" />
</svg>
<span class="d-none d-lg-inline"> Close</span>
</svg>&nbsp;<span class="d-none d-lg-inline" i18n>Close</span>
</button>
</app-page-header>
<div class="row">
<div class="col-xl">
<div class="col mb-4">
<form [formGroup]='documentForm' (ngSubmit)="save()">
<ul ngbNav #nav="ngbNav" class="nav-tabs">
<li [ngbNavItem]="1">
<a ngbNavLink>Details</a>
<a ngbNavLink i18n>Details</a>
<ng-template ngbNavContent>
<app-input-text title="Title" formControlName="title"></app-input-text>
<app-input-text i18n-title title="Title" formControlName="title"></app-input-text>
<div class="form-group">
<label for="archive_serial_number">Archive Serial Number</label>
<label for="archive_serial_number" i18n>Archive serial number</label>
<input type="number" class="form-control" id="archive_serial_number"
formControlName='archive_serial_number'>
</div>
<app-input-date-time titleDate="Date created" formControlName="created"></app-input-date-time>
<app-input-select [items]="correspondents" title="Correspondent" formControlName="correspondent"
allowNull="true" (createNew)="createCorrespondent()"></app-input-select>
<app-input-select [items]="documentTypes" title="Document type" formControlName="document_type"
allowNull="true" (createNew)="createDocumentType()"></app-input-select>
<app-input-tags formControlName="tags" title="Tags"></app-input-tags>
<app-input-date-time i18n-titleDate titleDate="Date created" formControlName="created"></app-input-date-time>
<app-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true"
(createNew)="createCorrespondent()"></app-input-select>
<app-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true"
(createNew)="createDocumentType()"></app-input-select>
<app-input-tags formControlName="tags" i18n-title title="Tags"></app-input-tags>
</ng-template>
</li>
<li [ngbNavItem]="2">
<a ngbNavLink>Content</a>
<a ngbNavLink i18n>Content</a>
<ng-template ngbNavContent>
<div class="form-group">
<textarea class="form-control" id="content" rows="20" formControlName='content'></textarea>
@@ -70,93 +82,48 @@
</li>
<li [ngbNavItem]="3">
<a ngbNavLink>Metadata</a>
<a ngbNavLink i18n>Metadata</a>
<ng-template ngbNavContent>
<table class="table table-borderless">
<tbody>
<tr>
<td>Date modified</td>
<td i18n>Date modified</td>
<td>{{document.modified | date:'medium'}}</td>
</tr>
<tr>
<td>Date added</td>
<td i18n>Date added</td>
<td>{{document.added | date:'medium'}}</td>
</tr>
<tr>
<td>Media filename</td>
<td i18n>Media filename</td>
<td>{{metadata?.media_filename}}</td>
</tr>
<tr>
<td>Original MD5 Checksum</td>
<td i18n>Original MD5 checksum</td>
<td>{{metadata?.original_checksum}}</td>
</tr>
<tr>
<td>Original file size</td>
<td i18n>Original file size</td>
<td>{{metadata?.original_size | fileSize}}</td>
</tr>
<tr>
<td>Original mime type</td>
<td i18n>Original mime type</td>
<td>{{metadata?.original_mime_type}}</td>
</tr>
<tr *ngIf="metadata?.has_archive_version">
<td>Archive MD5 Checksum</td>
<td i18n>Archive MD5 checksum</td>
<td>{{metadata?.archive_checksum}}</td>
</tr>
<tr *ngIf="metadata?.has_archive_version">
<td>Archive file size</td>
<td i18n>Archive file size</td>
<td>{{metadata?.archive_size | fileSize}}</td>
</tr>
</tbody>
</table>
<h6 *ngIf="metadata?.original_metadata.length > 0">
<button type="button" class="btn btn-outline-secondary btn-sm mr-2"
(click)="expandOriginalMetadata = !expandOriginalMetadata" aria-controls="collapseExample">
<svg class="buttonicon" fill="currentColor" *ngIf="!expandOriginalMetadata">
<use xlink:href="assets/bootstrap-icons.svg#caret-down" />
</svg>
<svg class="buttonicon" fill="currentColor" *ngIf="expandOriginalMetadata">
<use xlink:href="assets/bootstrap-icons.svg#caret-up" />
</svg>
</button>
Original document metadata
</h6>
<div #collapse="ngbCollapse" [(ngbCollapse)]="!expandOriginalMetadata">
<table class="table table-borderless">
<tbody>
<tr *ngFor="let m of metadata?.original_metadata">
<td>{{m.prefix}}:{{m.key}}</td>
<td>{{m.value}}</td>
</tr>
</tbody>
</table>
</div>
<h6 *ngIf="metadata?.has_archive_version && metadata?.archive_metadata.length > 0">
<button type="button" class="btn btn-outline-secondary btn-sm mr-2"
(click)="expandArchivedMetadata = !expandArchivedMetadata" aria-controls="collapseExample">
<svg class="buttonicon" fill="currentColor" *ngIf="!expandArchivedMetadata">
<use xlink:href="assets/bootstrap-icons.svg#caret-down" />
</svg>
<svg class="buttonicon" fill="currentColor" *ngIf="expandArchivedMetadata">
<use xlink:href="assets/bootstrap-icons.svg#caret-up" />
</svg>
</button>
Archived document metadata
</h6>
<div #collapse="ngbCollapse" [(ngbCollapse)]="!expandArchivedMetadata">
<table class="table table-borderless">
<tbody>
<tr *ngFor="let m of metadata?.archive_metadata">
<td>{{m.prefix}}:{{m.key}}</td>
<td>{{m.value}}</td>
</tr>
</tbody>
</table>
</div>
<app-metadata-collapse i18n-title title="Original document metadata" [metadata]="metadata.original_metadata" *ngIf="metadata?.original_metadata?.length > 0"></app-metadata-collapse>
<app-metadata-collapse i18n-title title="Archived document metadata" [metadata]="metadata.archive_metadata" *ngIf="metadata?.archive_metadata?.length > 0"></app-metadata-collapse>
</ng-template>
</li>
@@ -164,18 +131,15 @@
<div [ngbNavOutlet]="nav" class="mt-2"></div>
<button type="button" class="btn btn-outline-secondary" (click)="discard()">Discard</button>&nbsp;
<button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()">Save & edit
next</button>&nbsp;
<button type="submit" class="btn btn-primary">Save</button>&nbsp;
<button type="button" class="btn btn-outline-secondary" (click)="discard()" i18n>Discard</button>&nbsp;
<button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()" i18n>Save & next</button>&nbsp;
<button type="submit" class="btn btn-primary" i18n>Save</button>&nbsp;
</form>
</div>
<div class="col-xl d-none d-xl-block document-preview">
<object [data]="previewUrl | safe" type="application/pdf" width="100%" height="100%">
<p>Your browser does not support PDFs.
<a href="previewUrl">Download the PDF</a>.</p>
</object>
<div class="col-md-6 col-xl-8 mb-3">
<div class="pdf-viewer-container" *ngIf="getContentType() == 'application/pdf'">
<pdf-viewer [src]="previewUrl" [original-size]="false" [show-borders]="true" [show-all]="true" [(page)]="previewCurrentPage" [render-text-mode]="2" (after-load-complete)="pdfPreviewLoaded($event)"></pdf-viewer>
</div>
</div>
</div>
</div>

View File

@@ -1,5 +1,6 @@
.document-preview {
height: calc(100vh - 180px);
.pdf-viewer-container {
height: calc(100vh - 160px);
top: 70px;
position: sticky;
}
background-color: gray;
}

View File

@@ -1,21 +1,21 @@
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
import { PaperlessDocument } from 'src/app/data/paperless-document';
import { PaperlessDocumentMetadata } from 'src/app/data/paperless-document-metadata';
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe';
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
import { OpenDocumentsService } from 'src/app/services/open-documents.service';
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
import { DocumentTypeService } from 'src/app/services/rest/document-type.service';
import { DocumentService } from 'src/app/services/rest/document.service';
import { environment } from 'src/environments/environment';
import { DeleteDialogComponent } from '../common/delete-dialog/delete-dialog.component';
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component';
import { CorrespondentEditDialogComponent } from '../manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component';
import { DocumentTypeEditDialogComponent } from '../manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component';
import { PDFDocumentProxy } from 'ng2-pdf-viewer';
@Component({
selector: 'app-document-detail',
@@ -48,8 +48,11 @@ export class DocumentDetailComponent implements OnInit {
tags: new FormControl([])
})
previewCurrentPage: number = 1
previewNumPages: number = 1
constructor(
private documentsService: DocumentService,
private documentsService: DocumentService,
private route: ActivatedRoute,
private correspondentService: CorrespondentService,
private documentTypeService: DocumentTypeService,
@@ -57,7 +60,11 @@ export class DocumentDetailComponent implements OnInit {
private modalService: NgbModal,
private openDocumentService: OpenDocumentsService,
private documentListViewService: DocumentListViewService,
private titleService: Title) { }
private documentTitlePipe: DocumentTitlePipe) { }
getContentType() {
return this.metadata?.has_archive_version ? 'application/pdf' : this.metadata?.original_mime_type
}
ngOnInit(): void {
this.documentForm.valueChanges.subscribe(wow => {
@@ -86,11 +93,10 @@ export class DocumentDetailComponent implements OnInit {
updateComponent(doc: PaperlessDocument) {
this.document = doc
this.titleService.setTitle(`${doc.title} - ${environment.appTitle}`)
this.documentsService.getMetadata(doc.id).subscribe(result => {
this.metadata = result
})
this.title = doc.title
this.title = this.documentTitlePipe.transform(doc.title)
this.documentForm.patchValue(doc)
}
@@ -124,7 +130,7 @@ export class DocumentDetailComponent implements OnInit {
}, error => {this.router.navigate(['404'])})
}
save() {
save() {
this.documentsService.update(this.document).subscribe(result => {
this.close()
})
@@ -151,19 +157,31 @@ export class DocumentDetailComponent implements OnInit {
}
delete() {
let modal = this.modalService.open(DeleteDialogComponent, {backdrop: 'static'})
modal.componentInstance.message = `Do you really want to delete document '${this.document.title}'?`
modal.componentInstance.message2 = `The files for this document will be deleted permanently. This operation cannot be undone.`
modal.componentInstance.deleteClicked.subscribe(() => {
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
modal.componentInstance.title = $localize`Confirm delete`
modal.componentInstance.messageBold = $localize`Do you really want to delete document "${this.document.title}"?`
modal.componentInstance.message = $localize`The files for this document will be deleted permanently. This operation cannot be undone.`
modal.componentInstance.btnClass = "btn-danger"
modal.componentInstance.btnCaption = $localize`Delete document`
modal.componentInstance.confirmClicked.subscribe(() => {
this.documentsService.delete(this.document).subscribe(() => {
modal.close()
modal.close()
this.close()
})
})
}
moreLike() {
this.router.navigate(["search"], {queryParams: {more_like:this.document.id}})
}
hasNext() {
return this.documentListViewService.hasNext(this.documentId)
}
pdfPreviewLoaded(pdf: PDFDocumentProxy) {
this.previewNumPages = pdf.numPages
}
}

View File

@@ -0,0 +1,23 @@
<h6>
<button type="button" class="btn btn-outline-secondary btn-sm mr-2"
(click)="expand = !expand">
<svg class="buttonicon" fill="currentColor" *ngIf="!expand">
<use xlink:href="assets/bootstrap-icons.svg#caret-down" />
</svg>
<svg class="buttonicon" fill="currentColor" *ngIf="expand">
<use xlink:href="assets/bootstrap-icons.svg#caret-up" />
</svg>
</button>
{{title}}
</h6>
<div #collapse="ngbCollapse" [(ngbCollapse)]="!expand">
<table class="table table-borderless">
<tbody>
<tr *ngFor="let m of metadata">
<td>{{m.prefix}}:{{m.key}}</td>
<td>{{m.value}}</td>
</tr>
</tbody>
</table>
</div>

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MetadataCollapseComponent } from './metadata-collapse.component';
describe('MetadataCollapseComponent', () => {
let component: MetadataCollapseComponent;
let fixture: ComponentFixture<MetadataCollapseComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MetadataCollapseComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(MetadataCollapseComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,23 @@
import { Component, Input, OnInit } from '@angular/core';
@Component({
selector: 'app-metadata-collapse',
templateUrl: './metadata-collapse.component.html',
styleUrls: ['./metadata-collapse.component.scss']
})
export class MetadataCollapseComponent implements OnInit {
constructor() { }
expand = false
@Input()
metadata
@Input()
title = $localize`Metadata`
ngOnInit(): void {
}
}

View File

@@ -0,0 +1,67 @@
<div class="row">
<div class="col-auto mb-2 mb-xl-0" role="group" aria-label="Select">
<button class="btn btn-sm btn-outline-danger" (click)="list.selectNone()">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#slash-circle" />
</svg>&nbsp;<ng-container i18n>Cancel</ng-container>
</button>
</div>
<div class="w-100 d-xl-none"></div>
<div class="col-auto mb-2 mb-xl-0" role="group" aria-label="Select">
<label class="mr-2 mb-0" i18n>Select:</label>
<div class="btn-group">
<button class="btn btn-sm btn-outline-primary" (click)="list.selectPage()">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-earmark-check" />
</svg>&nbsp;<ng-container i18n>Page</ng-container>
</button>
<button class="btn btn-sm btn-outline-primary" (click)="list.selectAll()">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#check-all" />
</svg>&nbsp;<ng-container i18n>All</ng-container>
</button>
</div>
</div>
<div class="w-100 d-xl-none"></div>
<div class="col-auto mb-2 mb-xl-0">
<div class="d-flex">
<label class="ml-auto mt-1 mb-0 mr-2" i18n>Edit:</label>
<app-filterable-dropdown class="mr-2 mr-md-3" title="Tags" icon="tag-fill" i18n-title
filterPlaceholder="Filter tags" i18n-filterPlaceholder
[items]="tags"
[editing]="true"
[multiple]="true"
[applyOnClose]="applyOnClose"
(open)="openTagsDropdown()"
[(selectionModel)]="tagSelectionModel"
(apply)="setTags($event)">
</app-filterable-dropdown>
<app-filterable-dropdown class="mr-2 mr-md-3" title="Correspondent" icon="person-fill" i18n-title
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
[items]="correspondents"
[editing]="true"
[applyOnClose]="applyOnClose"
(open)="openCorrespondentDropdown()"
[(selectionModel)]="correspondentSelectionModel"
(apply)="setCorrespondents($event)">
</app-filterable-dropdown>
<app-filterable-dropdown class="mr-2 mr-md-3" title="Document type" icon="file-earmark-fill" i18n-title
filterPlaceholder="Filter document types" i18n-filterPlaceholder
[items]="documentTypes"
[editing]="true"
[applyOnClose]="applyOnClose"
(open)="openDocumentTypeDropdown()"
[(selectionModel)]="documentTypeSelectionModel"
(apply)="setDocumentTypes($event)">
</app-filterable-dropdown>
</div>
</div>
<div class="w-100 d-xl-none"></div>
<div class="col mb-2 mb-xl-0 d-flex">
<button type="button" class="btn btn-sm btn-outline-danger ml-0 ml-lg-auto" (click)="applyDelete()">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
</div>
</div>

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BulkEditorComponent } from './bulk-editor.component';
describe('BulkEditorComponent', () => {
let component: BulkEditorComponent;
let fixture: ComponentFixture<BulkEditorComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ BulkEditorComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(BulkEditorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,232 @@
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { PaperlessTag } from 'src/app/data/paperless-tag';
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
import { TagService } from 'src/app/services/rest/tag.service';
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
import { DocumentTypeService } from 'src/app/services/rest/document-type.service';
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { DocumentService, SelectionDataItem } from 'src/app/services/rest/document.service';
import { OpenDocumentsService } from 'src/app/services/open-documents.service';
import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component';
import { ChangedItems, FilterableDropdownSelectionModel } from '../../common/filterable-dropdown/filterable-dropdown.component';
import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component';
import { MatchingModel } from 'src/app/data/matching-model';
import { SettingsService, SETTINGS_KEYS } from 'src/app/services/settings.service';
@Component({
selector: 'app-bulk-editor',
templateUrl: './bulk-editor.component.html',
styleUrls: ['./bulk-editor.component.scss']
})
export class BulkEditorComponent {
tags: PaperlessTag[]
correspondents: PaperlessCorrespondent[]
documentTypes: PaperlessDocumentType[]
tagSelectionModel = new FilterableDropdownSelectionModel()
correspondentSelectionModel = new FilterableDropdownSelectionModel()
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
constructor(
private documentTypeService: DocumentTypeService,
private tagService: TagService,
private correspondentService: CorrespondentService,
public list: DocumentListViewService,
private documentService: DocumentService,
private modalService: NgbModal,
private openDocumentService: OpenDocumentsService,
private settings: SettingsService
) { }
applyOnClose: boolean = this.settings.get(SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE)
showConfirmationDialogs: boolean = this.settings.get(SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS)
ngOnInit() {
this.tagService.listAll().subscribe(result => this.tags = result.results)
this.correspondentService.listAll().subscribe(result => this.correspondents = result.results)
this.documentTypeService.listAll().subscribe(result => this.documentTypes = result.results)
}
private executeBulkOperation(method: string, args): Observable<any> {
return this.documentService.bulkEdit(Array.from(this.list.selected), method, args).pipe(
tap(() => {
this.list.reload()
this.list.reduceSelectionToFilter()
this.list.selected.forEach(id => {
this.openDocumentService.refreshDocument(id)
})
})
)
}
private applySelectionData(items: SelectionDataItem[], selectionModel: FilterableDropdownSelectionModel) {
let selectionData = new Map<number, ToggleableItemState>()
items.forEach(i => {
if (i.document_count == this.list.selected.size) {
selectionData.set(i.id, ToggleableItemState.Selected)
} else if (i.document_count > 0) {
selectionData.set(i.id, ToggleableItemState.PartiallySelected)
}
})
selectionModel.init(selectionData)
}
openTagsDropdown() {
this.documentService.getSelectionData(Array.from(this.list.selected)).subscribe(s => {
this.applySelectionData(s.selected_tags, this.tagSelectionModel)
})
}
openDocumentTypeDropdown() {
this.documentService.getSelectionData(Array.from(this.list.selected)).subscribe(s => {
this.applySelectionData(s.selected_document_types, this.documentTypeSelectionModel)
})
}
openCorrespondentDropdown() {
this.documentService.getSelectionData(Array.from(this.list.selected)).subscribe(s => {
this.applySelectionData(s.selected_correspondents, this.correspondentSelectionModel)
})
}
private _localizeList(items: MatchingModel[]) {
if (items.length == 0) {
return ""
} else if (items.length == 1) {
return items[0].name
} else if (items.length == 2) {
return $localize`:This is for messages like 'modify "tag1" and "tag2"':"${items[0].name}" and "${items[1].name}"`
} else {
let list = items.slice(0, items.length - 1).map(i => $localize`"${i.name}"`).join($localize`:this is used to separate enumerations and should probably be a comma and a whitespace in most languages:, `)
return $localize`:this is for messages like 'modify "tag1", "tag2" and "tag3"':${list} and "${items[items.length - 1].name}"`
}
}
setTags(changedTags: ChangedItems) {
if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length == 0) return
if (this.showConfirmationDialogs) {
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
modal.componentInstance.title = $localize`Confirm tags assignment`
if (changedTags.itemsToAdd.length == 1 && changedTags.itemsToRemove.length == 0) {
let tag = changedTags.itemsToAdd[0]
modal.componentInstance.message = $localize`This operation will add the tag "${tag.name}" to ${this.list.selected.size} selected document(s).`
} else if (changedTags.itemsToAdd.length > 1 && changedTags.itemsToRemove.length == 0) {
modal.componentInstance.message = $localize`This operation will add the tags ${this._localizeList(changedTags.itemsToAdd)} to ${this.list.selected.size} selected document(s).`
} else if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length == 1) {
let tag = changedTags.itemsToRemove[0]
modal.componentInstance.message = $localize`This operation will remove the tag "${tag.name}" from ${this.list.selected.size} selected document(s).`
} else if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length > 1) {
modal.componentInstance.message = $localize`This operation will remove the tags ${this._localizeList(changedTags.itemsToRemove)} from ${this.list.selected.size} selected document(s).`
} else {
modal.componentInstance.message = $localize`This operation will add the tags ${this._localizeList(changedTags.itemsToAdd)} and remove the tags ${this._localizeList(changedTags.itemsToRemove)} on ${this.list.selected.size} selected document(s).`
}
modal.componentInstance.btnClass = "btn-warning"
modal.componentInstance.btnCaption = $localize`Confirm`
modal.componentInstance.confirmClicked.subscribe(() => {
this.performSetTags(modal, changedTags)
})
} else {
this.performSetTags(null, changedTags)
}
}
private performSetTags(modal, changedTags: ChangedItems) {
this.executeBulkOperation('modify_tags', {"add_tags": changedTags.itemsToAdd.map(t => t.id), "remove_tags": changedTags.itemsToRemove.map(t => t.id)}).subscribe(
response => {
if (modal) {
modal.close()
}
}
)
}
setCorrespondents(changedCorrespondents: ChangedItems) {
if (changedCorrespondents.itemsToAdd.length == 0 && changedCorrespondents.itemsToRemove.length == 0) return
let correspondent = changedCorrespondents.itemsToAdd.length > 0 ? changedCorrespondents.itemsToAdd[0] : null
if (this.showConfirmationDialogs) {
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
modal.componentInstance.title = $localize`Confirm correspondent assignment`
if (correspondent) {
modal.componentInstance.message = $localize`This operation will assign the correspondent "${correspondent.name}" to ${this.list.selected.size} selected document(s).`
} else {
modal.componentInstance.message = $localize`This operation will remove the correspondent from ${this.list.selected.size} selected document(s).`
}
modal.componentInstance.btnClass = "btn-warning"
modal.componentInstance.btnCaption = $localize`Confirm`
modal.componentInstance.confirmClicked.subscribe(() => {
this.performSetCorrespondents(modal, correspondent)
})
} else {
this.performSetCorrespondents(null, correspondent)
}
}
private performSetCorrespondents(modal, correspondent: MatchingModel) {
this.executeBulkOperation('set_correspondent', {"correspondent": correspondent ? correspondent.id : null}).subscribe(
response => {
if (modal) {
modal.close()
}
}
)
}
setDocumentTypes(changedDocumentTypes: ChangedItems) {
if (changedDocumentTypes.itemsToAdd.length == 0 && changedDocumentTypes.itemsToRemove.length == 0) return
let documentType = changedDocumentTypes.itemsToAdd.length > 0 ? changedDocumentTypes.itemsToAdd[0] : null
if (this.showConfirmationDialogs) {
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
modal.componentInstance.title = $localize`Confirm document type assignment`
if (documentType) {
modal.componentInstance.message = $localize`This operation will assign the document type "${documentType.name}" to ${this.list.selected.size} selected document(s).`
} else {
modal.componentInstance.message = $localize`This operation will remove the document type from ${this.list.selected.size} selected document(s).`
}
modal.componentInstance.btnClass = "btn-warning"
modal.componentInstance.btnCaption = $localize`Confirm`
modal.componentInstance.confirmClicked.subscribe(() => {
this.performSetDocumentTypes(modal, documentType)
})
} else {
this.performSetDocumentTypes(null, documentType)
}
}
private performSetDocumentTypes(modal, documentType) {
this.executeBulkOperation('set_document_type', {"document_type": documentType ? documentType.id : null}).subscribe(
response => {
if (modal) {
modal.close()
}
}
)
}
applyDelete() {
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
modal.componentInstance.delayConfirm(5)
modal.componentInstance.title = $localize`Delete confirm`
modal.componentInstance.messageBold = $localize`This operation will permanently delete ${this.list.selected.size} selected document(s).`
modal.componentInstance.message = $localize`This operation cannot be undone.`
modal.componentInstance.btnClass = "btn-danger"
modal.componentInstance.btnCaption = $localize`Delete document(s)`
modal.componentInstance.confirmClicked.subscribe(() => {
this.executeBulkOperation("delete", {}).subscribe(
response => {
modal.close()
}
)
})
}
}

View File

@@ -1,19 +1,27 @@
<div class="card mb-3 bg-light shadow-sm">
<div class="card mb-3 bg-light shadow-sm" [class.card-selected]="selected" [class.document-card]="selectable">
<div class="row no-gutters">
<div class="col-md-2 d-none d-lg-block">
<img [src]="getThumbUrl()" class="card-img doc-img border-right">
<div class="col-md-2 d-none d-lg-block doc-img-background" [class.doc-img-background-selected]="selected">
<img [src]="getThumbUrl()" class="card-img doc-img border-right" (click)="setSelected(selectable ? !selected : false)">
<div style="top: 0; left: 0" class="position-absolute border-right border-bottom bg-light p-1" [class.document-card-check]="!selected">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="smallCardCheck{{document.id}}" [checked]="selected" (change)="setSelected($event.target.checked)">
<label class="custom-control-label" for="smallCardCheck{{document.id}}"></label>
</div>
</div>
</div>
<div class="col">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title">
<h5 class="card-title">
<ng-container *ngIf="document.correspondent">
<a *ngIf="clickCorrespondent.observers.length ; else nolink" [routerLink]="" title="Filter by correspondent" (click)="clickCorrespondent.emit(document.correspondent)" class="font-weight-bold">{{(document.correspondent$ | async)?.name}}</a>
<a *ngIf="clickCorrespondent.observers.length ; else nolink" [routerLink]="" title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent)" class="font-weight-bold">{{(document.correspondent$ | async)?.name}}</a>
<ng-template #nolink>{{(document.correspondent$ | async)?.name}}</ng-template>:
</ng-container>
{{document.title}}
<app-tag [tag]="t" linkTitle="Filter by tag" *ngFor="let t of document.tags$ | async" class="ml-1" (click)="clickTag.emit(t.id)" [clickable]="clickTag.observers.length"></app-tag>
{{document.title | documentTitle}}
<app-tag [tag]="t" linkTitle="Filter by tag" i18n-linkTitle *ngFor="let t of document.tags$ | async" class="ml-1" (click)="clickTag.emit(t.id)" [clickable]="clickTag.observers.length"></app-tag>
</h5>
<h5 class="card-title" *ngIf="document.archive_serial_number">#{{document.archive_serial_number}}</h5>
</div>
@@ -23,33 +31,41 @@
</p>
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<div class="btn-group">
<a routerLink="/search" [queryParams]="{'more_like': document.id}" class="btn btn-sm btn-outline-secondary" *ngIf="moreLikeThis">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-three-dots" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/>
</svg>&nbsp;<ng-container i18n>More like this</ng-container>
</a>
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
</svg>
Edit
</svg>&nbsp;<ng-container i18n>Edit</ng-container>
</a>
<a type="button" class="btn btn-sm btn-outline-secondary" [href]="getPreviewUrl()">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-search" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M10.442 10.442a1 1 0 0 1 1.415 0l3.85 3.85a1 1 0 0 1-1.414 1.415l-3.85-3.85a1 1 0 0 1 0-1.415z"/>
<path fill-rule="evenodd" d="M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0z"/>
</svg>
View
</svg>&nbsp;<ng-container i18n>View</ng-container>
</a>
<a type="button" class="btn btn-sm btn-outline-secondary" [href]="getDownloadUrl()">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-download" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path fill-rule="evenodd" d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>
Download
</svg>&nbsp;<ng-container i18n>Download</ng-container>
</a>
</div>
<small class="text-muted">Created: {{document.created | date}}</small>
</div>
<small class="text-muted ml-auto" i18n>Score:</small>
<ngb-progressbar *ngIf="searchScore" [type]="searchScoreClass" [value]="searchScore" class="search-score-bar mx-2" [max]="1"></ngb-progressbar>
<small class="text-muted" i18n>Created: {{document.created | date}}</small>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,5 +1,7 @@
@import "/src/theme";
.result-content {
color: darkgray;
overflow-wrap: anywhere;
}
.doc-img {
@@ -7,5 +9,31 @@
object-position: top;
height: 100%;
position: absolute;
mix-blend-mode: multiply;
}
.search-score-bar {
width: 100px;
height: 5px;
margin-top: 2px;
}
.document-card-check {
display: none
}
.document-card:hover .document-card-check {
display: block;
}
.card-selected {
border-color: $primary;
}
.doc-img-background {
background-color: white;
}
.doc-img-background-selected {
background-color: $primaryFaded;
}

View File

@@ -1,7 +1,6 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { PaperlessDocument } from 'src/app/data/paperless-document';
import { PaperlessTag } from 'src/app/data/paperless-tag';
import { DocumentService } from 'src/app/services/rest/document.service';
@Component({
@@ -13,6 +12,24 @@ export class DocumentCardLargeComponent implements OnInit {
constructor(private documentService: DocumentService, private sanitizer: DomSanitizer) { }
@Input()
selected = false
setSelected(value: boolean) {
this.selected = value
this.selectedChange.emit(value)
}
@Output()
selectedChange = new EventEmitter<boolean>()
get selectable() {
return this.selectedChange.observers.length > 0
}
@Input()
moreLikeThis: boolean = false
@Input()
document: PaperlessDocument
@@ -25,6 +42,19 @@ export class DocumentCardLargeComponent implements OnInit {
@Output()
clickCorrespondent = new EventEmitter<number>()
@Input()
searchScore: number
get searchScoreClass() {
if (this.searchScore > 0.7) {
return "success"
} else if (this.searchScore > 0.3) {
return "warning"
} else {
return "danger"
}
}
ngOnInit(): void {
}

View File

@@ -1,50 +1,58 @@
<div class="col p-2 h-100" style="width: 16rem;">
<div class="card h-100 shadow-sm">
<div class="border-bottom">
<img class="card-img doc-img" [src]="getThumbUrl()">
<div class="col p-2 h-100">
<div class="card h-100 shadow-sm document-card" [class.card-selected]="selected">
<div class="border-bottom" [class.doc-img-background-selected]="selected">
<img class="card-img doc-img" [src]="getThumbUrl()" (click)="setSelected(!selected)">
<div class="border-right border-bottom bg-light p-1 rounded document-card-check">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="smallCardCheck{{document.id}}" [checked]="selected" (change)="setSelected($event.target.checked)">
<label class="custom-control-label" for="smallCardCheck{{document.id}}"></label>
</div>
</div>
<div style="top: 0; right: 0; font-size: large" class="text-right position-absolute mr-1">
<div *ngFor="let t of getTagsLimited$() | async">
<app-tag [tag]="t" (click)="clickTag.emit(t.id)" [clickable]="true" linkTitle="Filter by tag"></app-tag>
<app-tag [tag]="t" (click)="clickTag.emit(t.id)" [clickable]="true" linkTitle="Filter by tag" i18n-linkTitle></app-tag>
</div>
<div *ngIf="moreTags">
<span class="badge badge-secondary">+ {{moreTags}}</span>
</div>
</div>
</div>
<div class="card-body p-2">
<p class="card-text">
<ng-container *ngIf="document.correspondent">
<a [routerLink]="" title="Filter by correspondent" (click)="clickCorrespondent.emit(document.correspondent)" class="font-weight-bold">{{(document.correspondent$ | async)?.name}}</a>:
<a [routerLink]="" title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent)" class="font-weight-bold">{{(document.correspondent$ | async)?.name}}</a>:
</ng-container>
{{document.title}}
{{document.title | documentTitle}}
</p>
</div>
<div class="card-footer">
<div class="d-flex justify-content-between align-items-center ml-n2">
<div class="d-flex justify-content-between align-items-center mx-n2">
<div class="btn-group">
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" title="Edit">
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" title="Edit" i18n-title>
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
</svg>
</a>
<a [href]="getPreviewUrl()" class="btn btn-sm btn-outline-secondary" title="View in browser">
<a [href]="getPreviewUrl()" class="btn btn-sm btn-outline-secondary" title="View in browser" i18n-title>
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-search" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M10.442 10.442a1 1 0 0 1 1.415 0l3.85 3.85a1 1 0 0 1-1.414 1.415l-3.85-3.85a1 1 0 0 1 0-1.415z"/>
<path fill-rule="evenodd" d="M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0z"/>
</svg>
</a>
<a [href]="getDownloadUrl()" class="btn btn-sm btn-outline-secondary" title="Download">
<a [href]="getDownloadUrl()" class="btn btn-sm btn-outline-secondary" title="Download" i18n-title>
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-download" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path fill-rule="evenodd" d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>
</a>
</div>
<small class="text-muted">{{document.created | date}}</small>
<small class="text-muted pl-1">{{document.created | date}}</small>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,5 +1,36 @@
@import "/src/theme";
.doc-img {
object-fit: cover;
object-position: top;
height: 200px;
}
mix-blend-mode: multiply;
}
.document-card-check {
display: none;
position: absolute;
top: 0;
left: 0;
.custom-control {
margin-left: 4px;
margin-right: -3px;
}
}
.document-card:hover .document-card-check {
display: block;
}
.card-selected {
border-color: $primary;
.document-card-check {
display: block;
}
}
.doc-img-background-selected {
background-color: $primaryFaded;
}

View File

@@ -1,7 +1,6 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { map } from 'rxjs/operators';
import { PaperlessDocument } from 'src/app/data/paperless-document';
import { PaperlessTag } from 'src/app/data/paperless-tag';
import { DocumentService } from 'src/app/services/rest/document.service';
@Component({
@@ -13,6 +12,17 @@ export class DocumentCardSmallComponent implements OnInit {
constructor(private documentService: DocumentService) { }
@Input()
selected = false
setSelected(value: boolean) {
this.selected = value
this.selectedChange.emit(value)
}
@Output()
selectedChange = new EventEmitter<boolean>()
@Input()
document: PaperlessDocument

View File

@@ -1,5 +1,19 @@
<app-page-header [title]="getTitle()">
<div ngbDropdown class="d-inline-block mr-2">
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
<svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#text-indent-left" />
</svg>&nbsp;<ng-container i18n>Select</ng-container>
</button>
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
<button ngbDropdownItem (click)="list.selectNone()" i18n>Select none</button>
<button ngbDropdownItem (click)="list.selectPage()" i18n>Select page</button>
<button ngbDropdownItem (click)="list.selectAll()" i18n>Select all</button>
</div>
</div>
<div class="btn-group btn-group-toggle" ngbRadioGroup [(ngModel)]="displayMode"
(ngModelChange)="saveDisplayMode()">
<label ngbButtonLabel class="btn-outline-primary btn-sm">
@@ -21,96 +35,98 @@
</svg>
</label>
</div>
<div class="btn-group btn-group-toggle ml-2" ngbRadioGroup [(ngModel)]="list.sortDirection">
<div class="btn-group btn-group-toggle ml-2" ngbRadioGroup [(ngModel)]="list.sortReverse">
<div ngbDropdown class="btn-group">
<button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle>Sort by</button>
<button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle i18n>Sort by</button>
<div ngbDropdownMenu aria-labelledby="dropdownBasic1" class="shadow">
<button *ngFor="let f of getSortFields()" ngbDropdownItem (click)="list.sortField = f.field"
[class.active]="list.sortField == f.field">{{f.name}}</button>
</div>
</div>
<label ngbButtonLabel class="btn-outline-primary btn-sm">
<input ngbButton type="radio" class="btn btn-sm" value="asc">
<input ngbButton type="radio" class="btn btn-sm" [value]="false">
<svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#sort-alpha-down" />
</svg>
</label>
<label ngbButtonLabel class="btn-outline-primary btn-sm">
<input ngbButton type="radio" class="btn btn-sm" value="des">
<input ngbButton type="radio" class="btn btn-sm" [value]="true">
<svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#sort-alpha-up-alt" />
</svg>
</label>
</div>
<div class="btn-group ml-2">
<button type="button" class="btn btn-sm" [ngClass]="isFiltered ? 'btn-primary' : 'btn-outline-primary'" (click)="showFilter=!showFilter">
<svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#funnel" />
</svg>
Filter
</button>
<div class="btn-group" ngbDropdown role="group">
<button class="btn btn-sm btn-outline-primary dropdown-toggle-split" ngbDropdownToggle></button>
<div class="dropdown-menu" ngbDropdownMenu class="shadow">
<ng-container *ngIf="!list.savedViewId" >
<button ngbDropdownItem *ngFor="let config of savedViewConfigService.getConfigs()" (click)="loadViewConfig(config)">{{config.title}}</button>
<div class="dropdown-divider" *ngIf="savedViewConfigService.getConfigs().length > 0"></div>
<button class="btn btn-sm btn-outline-primary dropdown-toggle" ngbDropdownToggle i18n>Views</button>
<div class="dropdown-menu shadow" ngbDropdownMenu>
<ng-container *ngIf="!list.savedViewId">
<button ngbDropdownItem *ngFor="let view of savedViewService.allViews" (click)="loadViewConfig(view)">{{view.name}}</button>
<div class="dropdown-divider" *ngIf="savedViewService.allViews.length > 0"></div>
</ng-container>
<button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.savedViewId">Save "{{list.savedViewTitle}}"</button>
<button ngbDropdownItem (click)="saveViewConfigAs()">Save as...</button>
<button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.savedViewId" i18n>Save "{{list.savedViewTitle}}"</button>
<button ngbDropdownItem (click)="saveViewConfigAs()" i18n>Save as...</button>
</div>
</div>
</div>
</app-page-header>
<div class="card w-100 mb-3" [hidden]="!showFilter">
<div class="card-body">
<h5 class="card-title">Filter</h5>
<app-filter-editor [(filterRules)]="filterRules" (apply)="applyFilterRules()" (clear)="clearFilterRules()"></app-filter-editor>
</div>
<div class="w-100 mb-2 mb-sm-4">
<app-filter-editor [hidden]="isBulkEditing" [(filterRules)]="list.filterRules" #filterEditor></app-filter-editor>
<app-bulk-editor [hidden]="!isBulkEditing"></app-bulk-editor>
</div>
<div class="d-flex justify-content-between align-items-center">
<p>{{list.collectionSize || 0}} document(s) <span *ngIf="isFiltered">(filtered)</span></p>
<p i18n *ngIf="list.selected.size > 0">Selected {{list.selected.size}} of {{list.collectionSize || 0}} {list.collectionSize, plural, =1 {document} other {documents}}</p>
<p *ngIf="list.selected.size == 0">{list.collectionSize, plural, =1 {1 document} other {{{list.collectionSize || 0}} documents}}</p>
<ngb-pagination [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
[rotate]="true" (pageChange)="list.reload()" aria-label="Default pagination"></ngb-pagination>
</div>
<div *ngIf="displayMode == 'largeCards'">
<app-document-card-large *ngFor="let d of list.documents" [document]="d" [details]="d.content" (clickTag)="filterByTag($event)" (clickCorrespondent)="filterByCorrespondent($event)">
<app-document-card-large [selected]="list.isSelected(d)" (selectedChange)="list.setSelected(d, $event)" *ngFor="let d of list.documents; trackBy: trackByDocumentId" [document]="d" [details]="d.content" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)">
</app-document-card-large>
</div>
<table class="table table-sm border shadow-sm" *ngIf="displayMode == 'details'">
<thead>
<th class="d-none d-lg-table-cell">ASN</th>
<th class="d-none d-md-table-cell">Correspondent</th>
<th>Title</th>
<th class="d-none d-xl-table-cell">Document type</th>
<th>Created</th>
<th class="d-none d-xl-table-cell">Added</th>
<th></th>
<th class="d-none d-lg-table-cell" i18n>ASN</th>
<th class="d-none d-md-table-cell" i18n>Correspondent</th>
<th i18n>Title</th>
<th class="d-none d-xl-table-cell" i18n>Document type</th>
<th i18n>Created</th>
<th class="d-none d-xl-table-cell" i18n>Added</th>
</thead>
<tbody>
<tr *ngFor="let d of list.documents">
<tr *ngFor="let d of list.documents; trackBy: trackByDocumentId" [ngClass]="list.isSelected(d) ? 'table-row-selected' : ''">
<td>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="docCheck{{d.id}}" [checked]="list.isSelected(d)" (change)="list.setSelected(d, $event.target.checked)">
<label class="custom-control-label" for="docCheck{{d.id}}"></label>
</div>
</td>
<td class="d-none d-lg-table-cell">
{{d.archive_serial_number}}
</td>
<td class="d-none d-md-table-cell">
<ng-container *ngIf="d.correspondent">
<a [routerLink]="" (click)="filterByCorrespondent(d.correspondent)" title="Filter by correspondent">{{(d.correspondent$ | async)?.name}}</a>
<a [routerLink]="" (click)="clickCorrespondent(d.correspondent)" title="Filter by correspondent">{{(d.correspondent$ | async)?.name}}</a>
</ng-container>
</td>
<td>
<a routerLink="/documents/{{d.id}}" title="Edit document" style="overflow-wrap: anywhere;">{{d.title}}</a>
<app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ml-1" clickable="true" linkTitle="Filter by tag" (click)="filterByTag(t.id)"></app-tag>
<a routerLink="/documents/{{d.id}}" title="Edit document" style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
<app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ml-1" clickable="true" linkTitle="Filter by tag" (click)="clickTag(t.id)"></app-tag>
</td>
<td class="d-none d-xl-table-cell">
<ng-container *ngIf="d.document_type">
<a [routerLink]="" (click)="filterByDocumentType(d.document_type)" title="Filter by document type">{{(d.document_type$ | async)?.name}}</a>
<a [routerLink]="" (click)="clickDocumentType(d.document_type)" title="Filter by document type">{{(d.document_type$ | async)?.name}}</a>
</ng-container>
</td>
<td>
@@ -123,7 +139,6 @@
</tbody>
</table>
<div class=" m-n2 row" *ngIf="displayMode == 'smallCards'">
<app-document-card-small [document]="d" *ngFor="let d of list.documents" (clickTag)="filterByTag($event)" (clickCorrespondent)="filterByCorrespondent($event)"></app-document-card-small>
<div class="m-n2 row row-cols-paperless-cards" *ngIf="displayMode == 'smallCards'">
<app-document-card-small [selected]="list.isSelected(d)" (selectedChange)="list.setSelected(d, $event)" [document]="d" *ngFor="let d of list.documents; trackBy: trackByDocumentId" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)"></app-document-card-small>
</div>

View File

@@ -0,0 +1,27 @@
@import "/src/theme";
.table-row-selected {
background-color: $primaryFaded;
}
$paperless-card-breakpoints: (
0: 2, // xs
768px: 3, //md
992px: 4, //lg
1200px: 5, //xl
1400px: 6, // xxl
1600px: 7,
1800px: 8,
2000px: 9
);
.row-cols-paperless-cards {
@each $width, $n_cols in $paperless-card-breakpoints {
@media(min-width: $width) {
> * {
flex: 0 0 auto;
width: 100% / $n_cols;
}
}
}
}

View File

@@ -1,15 +1,13 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute } from '@angular/router';
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { cloneFilterRules, FilterRule } from 'src/app/data/filter-rule';
import { FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type';
import { SavedViewConfig } from 'src/app/data/saved-view-config';
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 { DOCUMENT_SORT_FIELDS } from 'src/app/services/rest/document.service';
import { SavedViewConfigService } from 'src/app/services/saved-view-config.service';
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
import { Toast, ToastService } from 'src/app/services/toast.service';
import { environment } from 'src/environments/environment';
import { FilterEditorComponent } from './filter-editor/filter-editor.component';
import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component';
@Component({
@@ -21,29 +19,33 @@ export class DocumentListComponent implements OnInit {
constructor(
public list: DocumentListViewService,
public savedViewConfigService: SavedViewConfigService,
public savedViewService: SavedViewService,
public route: ActivatedRoute,
private router: Router,
private toastService: ToastService,
public modalService: NgbModal,
private titleService: Title) { }
private modalService: NgbModal) { }
@ViewChild("filterEditor")
private filterEditor: FilterEditorComponent
displayMode = 'smallCards' // largeCards, smallCards, details
filterRules: FilterRule[] = []
showFilter = false
get isFiltered() {
return this.list.filterRules?.length > 0
}
getTitle() {
return this.list.savedViewTitle || "Documents"
return this.list.savedViewTitle || $localize`Documents`
}
getSortFields() {
return DOCUMENT_SORT_FIELDS
}
get isBulkEditing(): boolean {
return this.list.selected.size > 0
}
saveDisplayMode() {
localStorage.setItem('document-list:displayMode', this.displayMode)
}
@@ -53,93 +55,78 @@ export class DocumentListComponent implements OnInit {
this.displayMode = localStorage.getItem('document-list:displayMode')
}
this.route.paramMap.subscribe(params => {
this.list.clear()
if (params.has('id')) {
this.list.savedView = this.savedViewConfigService.getConfig(params.get('id'))
this.filterRules = this.list.filterRules
this.showFilter = false
this.titleService.setTitle(`${this.list.savedView.title} - ${environment.appTitle}`)
this.savedViewService.getCached(+params.get('id')).subscribe(view => {
if (!view) {
this.router.navigate(["404"])
return
}
this.list.savedView = view
this.list.reload()
})
} else {
this.list.savedView = null
this.filterRules = this.list.filterRules
this.showFilter = this.filterRules.length > 0
this.titleService.setTitle(`Documents - ${environment.appTitle}`)
this.list.reload()
}
this.list.clear()
this.list.reload()
})
}
applyFilterRules() {
this.list.filterRules = this.filterRules
}
clearFilterRules() {
this.list.filterRules = this.filterRules
this.showFilter = false
}
loadViewConfig(config: SavedViewConfig) {
this.filterRules = cloneFilterRules(config.filterRules)
this.list.load(config)
loadViewConfig(view: PaperlessSavedView) {
this.list.load(view)
this.list.reload()
}
saveViewConfig() {
this.savedViewConfigService.updateConfig(this.list.savedView)
this.toastService.showToast(Toast.make("Information", `View "${this.list.savedView.title}" saved successfully.`))
this.savedViewService.update(this.list.savedView).subscribe(result => {
this.toastService.showInfo($localize`View "${this.list.savedView.name}" saved successfully.`)
})
}
saveViewConfigAs() {
let modal = this.modalService.open(SaveViewConfigDialogComponent, {backdrop: 'static'})
modal.componentInstance.defaultName = this.filterEditor.generateFilterName()
modal.componentInstance.saveClicked.subscribe(formValue => {
this.savedViewConfigService.newConfig({
title: formValue.title,
showInDashboard: formValue.showInDashboard,
showInSideBar: formValue.showInSideBar,
filterRules: this.list.filterRules,
sortDirection: this.list.sortDirection,
sortField: this.list.sortField
let savedView = {
name: formValue.name,
show_on_dashboard: formValue.showOnDashboard,
show_in_sidebar: formValue.showInSideBar,
filter_rules: this.list.filterRules,
sort_reverse: this.list.sortReverse,
sort_field: this.list.sortField
}
this.savedViewService.create(savedView).subscribe(() => {
modal.close()
this.toastService.showInfo($localize`View "${savedView.name}" created successfully.`)
})
modal.close()
})
}
filterByTag(tag_id: number) {
let filterRules = this.list.filterRules
if (filterRules.find(rule => rule.type.id == FILTER_HAS_TAG && rule.value == tag_id)) {
return
}
filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_HAS_TAG), value: tag_id})
this.filterRules = filterRules
this.applyFilterRules()
clickTag(tagID: number) {
this.list.selectNone()
setTimeout(() => {
this.filterEditor.toggleTag(tagID)
})
}
filterByCorrespondent(correspondent_id: number) {
let filterRules = this.list.filterRules
let existing_rule = filterRules.find(rule => rule.type.id == FILTER_CORRESPONDENT)
if (existing_rule && existing_rule.value == correspondent_id) {
return
} else if (existing_rule) {
existing_rule.value = correspondent_id
} else {
filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_CORRESPONDENT), value: correspondent_id})
}
this.filterRules = filterRules
this.applyFilterRules()
clickCorrespondent(correspondentID: number) {
this.list.selectNone()
setTimeout(() => {
this.filterEditor.toggleCorrespondent(correspondentID)
})
}
filterByDocumentType(document_type_id: number) {
let filterRules = this.list.filterRules
let existing_rule = filterRules.find(rule => rule.type.id == FILTER_DOCUMENT_TYPE)
if (existing_rule && existing_rule.value == document_type_id) {
return
} else if (existing_rule) {
existing_rule.value = document_type_id
} else {
filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_DOCUMENT_TYPE), value: document_type_id})
}
this.filterRules = filterRules
this.applyFilterRules()
clickDocumentType(documentTypeID: number) {
this.list.selectNone()
setTimeout(() => {
this.filterEditor.toggleDocumentType(documentTypeID)
})
}
trackByDocumentId(index, item: PaperlessDocument) {
return item.id
}
}

View File

@@ -0,0 +1,51 @@
<div class="row">
<div class="col mb-2 mb-xl-0">
<div class="form-inline d-flex">
<label class="text-muted mr-2" i18n>Filter by:</label>
<input class="form-control form-control-sm flex-grow-1" type="text" [(ngModel)]="titleFilter" placeholder="Title" i18n-placeholder>
</div>
</div>
<div class="w-100 d-xl-none"></div>
<div class="col col-xl-auto mb-2 mb-xl-0">
<div class="d-flex">
<app-filterable-dropdown class="mr-2 mr-md-3" title="Tags" icon="tag-fill" i18n-title
filterPlaceholder="Filter tags" i18n-filterPlaceholder
[items]="tags"
[(selectionModel)]="tagSelectionModel"
(selectionModelChange)="updateRules()"
[multiple]="true"
[allowSelectNone]="true"></app-filterable-dropdown>
<app-filterable-dropdown class="mr-2 mr-md-3" title="Correspondent" icon="person-fill" i18n-title
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
[items]="correspondents"
[(selectionModel)]="correspondentSelectionModel"
(selectionModelChange)="updateRules()"
[allowSelectNone]="true"></app-filterable-dropdown>
<app-filterable-dropdown class="mr-2 mr-md-3" title="Document type" icon="file-earmark-fill" i18n-title
filterPlaceholder="Filter document types" i18n-filterPlaceholder
[items]="documentTypes"
[(selectionModel)]="documentTypeSelectionModel"
(selectionModelChange)="updateRules()"
[allowSelectNone]="true"></app-filterable-dropdown>
<app-date-dropdown class="mr-2 mr-md-3"
title="Created" i18n-title
(datesSet)="updateRules()"
[(dateBefore)]="dateCreatedBefore"
[(dateAfter)]="dateCreatedAfter"></app-date-dropdown>
<app-date-dropdown
[(dateBefore)]="dateAddedBefore"
[(dateAfter)]="dateAddedAfter"
title="Added" i18n-title
(datesSet)="updateRules()"></app-date-dropdown>
</div>
</div>
<div class="w-100 d-xl-none"></div>
<div class="col col-xl-auto mb-2 mb-xl-0">
<button class="btn btn-link btn-sm px-0 mx-0 ml-xl-n4" [disabled]="!hasFilters()" (click)="clearSelected()">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
</svg>&nbsp;<ng-container i18n>Clear all filters</ng-container>
</button>
</div>
</div>

View File

@@ -0,0 +1,10 @@
.quick-filter {
min-width: 250px;
max-height: 400px;
overflow-y: scroll;
.selected-icon {
min-width: 1em;
min-height: 1em;
}
}

View File

@@ -0,0 +1,203 @@
import { Component, EventEmitter, Input, Output, OnInit, OnDestroy } from '@angular/core';
import { PaperlessTag } from 'src/app/data/paperless-tag';
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
import { Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { DocumentTypeService } from 'src/app/services/rest/document-type.service';
import { TagService } from 'src/app/services/rest/tag.service';
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
import { FilterRule } from 'src/app/data/filter-rule';
import { FILTER_ADDED_AFTER, FILTER_ADDED_BEFORE, FILTER_CORRESPONDENT, FILTER_CREATED_AFTER, FILTER_CREATED_BEFORE, FILTER_DOCUMENT_TYPE, FILTER_HAS_ANY_TAG, FILTER_HAS_TAG, FILTER_TITLE } from 'src/app/data/filter-rule-type';
import { FilterableDropdownSelectionModel } from '../../common/filterable-dropdown/filterable-dropdown.component';
import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component';
@Component({
selector: 'app-filter-editor',
templateUrl: './filter-editor.component.html',
styleUrls: ['./filter-editor.component.scss']
})
export class FilterEditorComponent implements OnInit, OnDestroy {
generateFilterName() {
if (this.filterRules.length == 1) {
let rule = this.filterRules[0]
switch(this.filterRules[0].rule_type) {
case FILTER_CORRESPONDENT:
return $localize`Correspondent: ${this.correspondents.find(c => c.id == +rule.value)?.name}`
case FILTER_DOCUMENT_TYPE:
return $localize`Type: ${this.documentTypes.find(dt => dt.id == +rule.value)?.name}`
case FILTER_HAS_TAG:
return $localize`Tag: ${this.tags.find(t => t.id == +rule.value)?.name}`
}
}
return ""
}
constructor(
private documentTypeService: DocumentTypeService,
private tagService: TagService,
private correspondentService: CorrespondentService
) { }
tags: PaperlessTag[] = []
correspondents: PaperlessCorrespondent[] = []
documentTypes: PaperlessDocumentType[] = []
_titleFilter = ""
tagSelectionModel = new FilterableDropdownSelectionModel()
correspondentSelectionModel = new FilterableDropdownSelectionModel()
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
dateCreatedBefore: string
dateCreatedAfter: string
dateAddedBefore: string
dateAddedAfter: string
@Input()
set filterRules (value: FilterRule[]) {
this.documentTypeSelectionModel.clear(false)
this.tagSelectionModel.clear(false)
this.correspondentSelectionModel.clear(false)
value.forEach(rule => {
switch (rule.rule_type) {
case FILTER_TITLE:
this._titleFilter = rule.value
break
case FILTER_CREATED_AFTER:
this.dateCreatedAfter = rule.value
break
case FILTER_CREATED_BEFORE:
this.dateCreatedBefore = rule.value
break
case FILTER_ADDED_AFTER:
this.dateAddedAfter = rule.value
break
case FILTER_ADDED_BEFORE:
this.dateAddedBefore = rule.value
break
case FILTER_HAS_TAG:
this.tagSelectionModel.set(rule.value ? +rule.value : null, ToggleableItemState.Selected, false)
break
case FILTER_HAS_ANY_TAG:
this.tagSelectionModel.set(null, ToggleableItemState.Selected, false)
break
case FILTER_CORRESPONDENT:
this.correspondentSelectionModel.set(rule.value ? +rule.value : null, ToggleableItemState.Selected, false)
break
case FILTER_DOCUMENT_TYPE:
this.documentTypeSelectionModel.set(rule.value ? +rule.value : null, ToggleableItemState.Selected, false)
break
}
})
}
get filterRules() {
let filterRules: FilterRule[] = []
if (this._titleFilter) {
filterRules.push({rule_type: FILTER_TITLE, value: this._titleFilter})
}
if (this.tagSelectionModel.isNoneSelected()) {
filterRules.push({rule_type: FILTER_HAS_ANY_TAG, value: "false"})
} else {
this.tagSelectionModel.getSelectedItems().filter(tag => tag.id).forEach(tag => {
filterRules.push({rule_type: FILTER_HAS_TAG, value: tag.id?.toString()})
})
}
this.correspondentSelectionModel.getSelectedItems().forEach(correspondent => {
filterRules.push({rule_type: FILTER_CORRESPONDENT, value: correspondent.id?.toString()})
})
this.documentTypeSelectionModel.getSelectedItems().forEach(documentType => {
filterRules.push({rule_type: FILTER_DOCUMENT_TYPE, value: documentType.id?.toString()})
})
if (this.dateCreatedBefore) {
filterRules.push({rule_type: FILTER_CREATED_BEFORE, value: this.dateCreatedBefore})
}
if (this.dateCreatedAfter) {
filterRules.push({rule_type: FILTER_CREATED_AFTER, value: this.dateCreatedAfter})
}
if (this.dateAddedBefore) {
filterRules.push({rule_type: FILTER_ADDED_BEFORE, value: this.dateAddedBefore})
}
if (this.dateAddedAfter) {
filterRules.push({rule_type: FILTER_ADDED_AFTER, value: this.dateAddedAfter})
}
return filterRules
}
@Output()
filterRulesChange = new EventEmitter<FilterRule[]>()
updateRules() {
this.filterRulesChange.next(this.filterRules)
}
hasFilters() {
return this._titleFilter ||
this.dateAddedAfter || this.dateAddedBefore || this.dateCreatedAfter || this.dateCreatedBefore ||
this.tagSelectionModel.selectionSize() || this.correspondentSelectionModel.selectionSize() || this.documentTypeSelectionModel.selectionSize()
}
get titleFilter() {
return this._titleFilter
}
set titleFilter(value) {
this.titleFilterDebounce.next(value)
}
titleFilterDebounce: Subject<string>
subscription: Subscription
ngOnInit() {
this.tagService.listAll().subscribe(result => this.tags = result.results)
this.correspondentService.listAll().subscribe(result => this.correspondents = result.results)
this.documentTypeService.listAll().subscribe(result => this.documentTypes = result.results)
this.titleFilterDebounce = new Subject<string>()
this.subscription = this.titleFilterDebounce.pipe(
debounceTime(400),
distinctUntilChanged()
).subscribe(title => {
this._titleFilter = title
this.updateRules()
})
}
ngOnDestroy() {
this.titleFilterDebounce.complete()
}
clearSelected() {
this._titleFilter = ""
this.tagSelectionModel.clear(false)
this.documentTypeSelectionModel.clear(false)
this.correspondentSelectionModel.clear(false)
this.dateAddedBefore = null
this.dateAddedAfter = null
this.dateCreatedBefore = null
this.dateCreatedAfter = null
this.updateRules()
}
toggleTag(tagId: number) {
this.tagSelectionModel.toggle(tagId)
}
toggleCorrespondent(correspondentId: number) {
this.correspondentSelectionModel.toggle(correspondentId)
}
toggleDocumentType(documentTypeId: number) {
this.documentTypeSelectionModel.toggle(documentTypeId)
}
}

View File

@@ -1,17 +1,17 @@
<form [formGroup]="saveViewConfigForm" class="needs-validation" novalidate (ngSubmit)="save()">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">Save current view</h4>
<h4 class="modal-title" id="modal-basic-title" i18n>Save current view</h4>
<button type="button" class="close" aria-label="Close" (click)="cancel()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<app-input-text title="Title" formControlName="title"></app-input-text>
<app-input-check title="Show in side bar" formControlName="showInSideBar"></app-input-check>
<app-input-check title="Show in dashboard" formControlName="showInDashboard"></app-input-check>
<app-input-text i18n-title title="Name" formControlName="name"></app-input-text>
<app-input-check i18n-title title="Show in sidebar" formControlName="showInSideBar"></app-input-check>
<app-input-check i18n-title title="Show on dashboard" formControlName="showOnDashboard"></app-input-check>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n>Cancel</button>
<button type="submit" class="btn btn-primary" i18n>Save</button>
</div>
</form>

View File

@@ -1,4 +1,4 @@
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
@@ -14,10 +14,23 @@ export class SaveViewConfigDialogComponent implements OnInit {
@Output()
public saveClicked = new EventEmitter()
_defaultName = ""
get defaultName() {
return this._defaultName
}
@Input()
set defaultName(value: string) {
this._defaultName = value
this.saveViewConfigForm.patchValue({name: value})
}
saveViewConfigForm = new FormGroup({
title: new FormControl(''),
name: new FormControl(''),
showInSideBar: new FormControl(false),
showInDashboard: new FormControl(false),
showOnDashboard: new FormControl(false),
})
ngOnInit(): void {

View File

@@ -1,52 +0,0 @@
<div *ngFor="let rule of filterRules" class="form-row form-group">
<div class="col-md-3 col-form-label">
<span>{{rule.type.name}}</span>
</div>
<div class="col">
<input *ngIf="rule.type.datatype == 'string'" type="text" class="form-control form-control-sm" [(ngModel)]="rule.value">
<input *ngIf="rule.type.datatype == 'number'" type="number" class="form-control form-control-sm" [(ngModel)]="rule.value">
<input *ngIf="rule.type.datatype == 'date'" type="date" class="form-control form-control-sm" [(ngModel)]="rule.value">
<select *ngIf="rule.type.datatype == 'tag'" class="form-control form-control-sm" [(ngModel)]="rule.value">
<option *ngFor="let t of tags" [ngValue]="t.id">{{t.name}}</option>
</select>
<select *ngIf="rule.type.datatype == 'document_type'" class="form-control form-control-sm" [(ngModel)]="rule.value">
<option *ngFor="let dt of documentTypes" [ngValue]="dt.id">{{dt.name}}</option>
</select>
<select *ngIf="rule.type.datatype == 'correspondent'" class="form-control form-control-sm" [(ngModel)]="rule.value">
<option *ngFor="let c of correspondents" [ngValue]="c.id">{{c.name}}</option>
</select>
<select *ngIf="rule.type.datatype == 'boolean'" class="form-control form-control-sm" [(ngModel)]="rule.value">
<option [ngValue]="true">Yes</option>
<option [ngValue]="false">No</option>
</select>
</div>
<div class="col-auto">
<button class="btn btn-sm btn-outline-secondary" (click)="removeRuleClicked(rule)">
<svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
</button>
</div>
</div>
<div class="form-row form-group">
<div class="col">
<select [(ngModel)]="selectedRuleType" class="form-control form-control-sm">
<option *ngFor="let ruleType of getRuleTypes()" [ngValue]="ruleType">{{ruleType.name}}</option>
</select>
</div>
<div class="col-auto">
<button (click)="newRuleClicked()" class="btn btn-sm btn-outline-secondary">Add</button>
</div>
<div class="col-auto">
<button (click)="clearClicked()" class="btn btn-sm btn-outline-secondary">Clear</button>
</div>
<div class="col-auto">
<button (click)="applyClicked()" class="btn btn-sm btn-outline-secondary">Apply</button>
</div>
</div>

View File

@@ -1,67 +0,0 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FilterRule } from 'src/app/data/filter-rule';
import { FilterRuleType, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type';
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
import { PaperlessTag } from 'src/app/data/paperless-tag';
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
import { DocumentTypeService } from 'src/app/services/rest/document-type.service';
import { TagService } from 'src/app/services/rest/tag.service';
@Component({
selector: 'app-filter-editor',
templateUrl: './filter-editor.component.html',
styleUrls: ['./filter-editor.component.scss']
})
export class FilterEditorComponent implements OnInit {
constructor(private documentTypeService: DocumentTypeService, private tagService: TagService, private correspondentService: CorrespondentService) { }
@Output()
clear = new EventEmitter()
@Input()
filterRules: FilterRule[] = []
@Output()
apply = new EventEmitter()
selectedRuleType: FilterRuleType = FILTER_RULE_TYPES[0]
correspondents: PaperlessCorrespondent[] = []
tags: PaperlessTag[] = []
documentTypes: PaperlessDocumentType[] = []
newRuleClicked() {
this.filterRules.push({type: this.selectedRuleType, value: this.selectedRuleType.default})
this.selectedRuleType = this.getRuleTypes().length > 0 ? this.getRuleTypes()[0] : null
}
removeRuleClicked(rule) {
let index = this.filterRules.findIndex(r => r == rule)
if (index > -1) {
this.filterRules.splice(index, 1)
}
}
applyClicked() {
this.apply.next()
}
clearClicked() {
this.filterRules.splice(0,this.filterRules.length)
this.clear.next()
}
ngOnInit(): void {
this.correspondentService.listAll().subscribe(result => {this.correspondents = result.results})
this.tagService.listAll().subscribe(result => this.tags = result.results)
this.documentTypeService.listAll().subscribe(result => this.documentTypes = result.results)
}
getRuleTypes() {
return FILTER_RULE_TYPES.filter(rt => rt.multi || !this.filterRules.find(r => r.type == rt))
}
}

View File

@@ -7,14 +7,13 @@
</div>
<div class="modal-body">
<app-input-text title="Name" formControlName="name"></app-input-text>
<app-input-text title="Match" formControlName="match"></app-input-text>
<app-input-select title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
<app-input-check title="Case insensitive" formControlName="is_insensitive"></app-input-check>
<app-input-text i18n-title title="Name" formControlName="name"></app-input-text>
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
<app-input-text i18n-title title="Match" formControlName="match" i18n-hint hint="Auto matching does not require you to fill in this field."></app-input-text>
<app-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" i18n-hint hint="Auto matching ignores this option."></app-input-check>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n>Cancel</button>
<button type="submit" class="btn btn-primary" i18n>Save</button>
</div>
</form>

View File

@@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Component } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component';
@@ -14,7 +14,19 @@ import { ToastService } from 'src/app/services/toast.service';
export class CorrespondentEditDialogComponent extends EditDialogComponent<PaperlessCorrespondent> {
constructor(service: CorrespondentService, activeModal: NgbActiveModal, toastService: ToastService) {
super(service, activeModal, toastService, 'correspondent')
super(service, activeModal, toastService)
}
getCreateTitle() {
return $localize`Create new correspondent`
}
getEditTitle() {
return $localize`Edit correspondent`
}
getSaveErrorMessage(error: string) {
return $localize`Could not save correspondent: ${error}`
}
getForm(): FormGroup {

View File

@@ -1,7 +1,5 @@
<app-page-header title="Correspondents">
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()">
Create
</button>
<app-page-header title="Correspondents" i18n-title>
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" i18n>Create</button>
</app-page-header>
<div class="row m-0 justify-content-end">
@@ -11,11 +9,11 @@
<table class="table table-striped border shadow">
<thead>
<tr>
<th scope="col" sortable="name" (sort)="onSort($event)">Name</th>
<th scope="col" sortable="matching_algorithm" (sort)="onSort($event)">Matching</th>
<th scope="col" sortable="document_count" (sort)="onSort($event)">Document count</th>
<th scope="col" sortable="last_correspondence" (sort)="onSort($event)">Last correspondence</th>
<th scope="col">Actions</th>
<th scope="col" sortable="name" (sort)="onSort($event)" i18n>Name</th>
<th scope="col" sortable="matching_algorithm" (sort)="onSort($event)" i18n>Matching</th>
<th scope="col" sortable="document_count" (sort)="onSort($event)" i18n>Document count</th>
<th scope="col" sortable="last_correspondence" (sort)="onSort($event)" i18n>Last correspondence</th>
<th scope="col" i18n>Actions</th>
</tr>
</thead>
<tbody>
@@ -26,9 +24,23 @@
<td scope="row">{{ correspondent.last_correspondence | date }}</td>
<td scope="row">
<div class="btn-group">
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(correspondent)">Edit</button>
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(correspondent)">Delete</button>
</div>
<button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(correspondent)">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-funnel" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2h-11z"/>
</svg>&nbsp;<ng-container i18n>Documents</ng-container>
</button>
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(correspondent)">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
</svg>&nbsp;<ng-container i18n>Edit</ng-container>
</button>
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(correspondent)">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
</svg>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
</div>
</td>
</tr>
</tbody>

View File

@@ -1,9 +1,9 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { Component } from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { FILTER_CORRESPONDENT } from 'src/app/data/filter-rule-type';
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
import { environment } from 'src/environments/environment';
import { GenericListComponent } from '../generic-list/generic-list.component';
import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog/correspondent-edit-dialog.component';
@@ -12,19 +12,19 @@ import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog/co
templateUrl: './correspondent-list.component.html',
styleUrls: ['./correspondent-list.component.scss']
})
export class CorrespondentListComponent extends GenericListComponent<PaperlessCorrespondent> implements OnInit {
export class CorrespondentListComponent extends GenericListComponent<PaperlessCorrespondent> {
constructor(correspondentsService: CorrespondentService, modalService: NgbModal, private titleService: Title) {
constructor(correspondentsService: CorrespondentService, modalService: NgbModal,
private list: DocumentListViewService
) {
super(correspondentsService,modalService,CorrespondentEditDialogComponent)
}
getObjectName(object: PaperlessCorrespondent) {
return `correspondent '${object.name}'`
getDeleteMessage(object: PaperlessCorrespondent) {
return $localize`Do you really want to delete the correspondent "${object.name}"?`
}
ngOnInit(): void {
super.ngOnInit()
this.titleService.setTitle(`Correspondents - ${environment.appTitle}`)
filterDocuments(object: PaperlessCorrespondent) {
this.list.quickFilter([{rule_type: FILTER_CORRESPONDENT, value: object.id.toString()}])
}
}

View File

@@ -7,14 +7,14 @@
</div>
<div class="modal-body">
<app-input-text title="Name" formControlName="name"></app-input-text>
<app-input-text title="Match" formControlName="match"></app-input-text>
<app-input-select title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
<app-input-check title="Case insensitive" formControlName="is_insensitive"></app-input-check>
<app-input-text i18n-title title="Name" formControlName="name"></app-input-text>
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
<app-input-text i18n-title title="Match" formControlName="match" i18n-hint hint="Auto matching does not require you to fill in this field."></app-input-text>
<app-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" i18n-hint hint="Auto matching ignores this option."></app-input-check>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n>Cancel</button>
<button type="submit" class="btn btn-primary" i18n>Save</button>
</div>
</form>

View File

@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { Component } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component';
@@ -14,7 +14,19 @@ import { ToastService } from 'src/app/services/toast.service';
export class DocumentTypeEditDialogComponent extends EditDialogComponent<PaperlessDocumentType> {
constructor(service: DocumentTypeService, activeModal: NgbActiveModal, toastService: ToastService) {
super(service, activeModal, toastService, 'document type')
super(service, activeModal, toastService)
}
getCreateTitle() {
return $localize`Create new document type`
}
getEditTitle() {
return $localize`Edit document type`
}
getSaveErrorMessage(error: string) {
return $localize`Could not save document type: ${error}`
}
getForm(): FormGroup {

View File

@@ -1,7 +1,5 @@
<app-page-header title="Document types">
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()">
Create
</button>
<app-page-header title="Document types" i18n-title>
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" i18n>Create</button>
</app-page-header>
<div class="row m-0 justify-content-end">
@@ -12,10 +10,10 @@
<table class="table table-striped border shadow">
<thead>
<tr>
<th scope="col" sortable="name" (sort)="onSort($event)">Name</th>
<th scope="col" sortable="matching_algorithm" (sort)="onSort($event)">Matching</th>
<th scope="col" sortable="document_count" (sort)="onSort($event)">Document count</th>
<th scope="col">Actions</th>
<th scope="col" sortable="name" (sort)="onSort($event)" i18n>Name</th>
<th scope="col" sortable="matching_algorithm" (sort)="onSort($event)" i18n>Matching</th>
<th scope="col" sortable="document_count" (sort)="onSort($event)" i18n>Document count</th>
<th scope="col" i18n>Actions</th>
</tr>
</thead>
<tbody>
@@ -25,8 +23,22 @@
<td scope="row">{{ document_type.document_count }}</td>
<td scope="row">
<div class="btn-group">
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(document_type)">Edit</button>
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(document_type)">Delete</button>
<button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(document_type)">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-funnel" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2h-11z"/>
</svg>&nbsp;<ng-container i18n>Documents</ng-container>
</button>
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(document_type)">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
</svg>&nbsp;<ng-container i18n>Edit</ng-container>
</button>
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(document_type)">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
</svg>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
</div>
</td>
</tr>

View File

@@ -1,9 +1,9 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { Component } from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { FILTER_DOCUMENT_TYPE } from 'src/app/data/filter-rule-type';
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
import { DocumentTypeService } from 'src/app/services/rest/document-type.service';
import { environment } from 'src/environments/environment';
import { GenericListComponent } from '../generic-list/generic-list.component';
import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog/document-type-edit-dialog.component';
@@ -12,18 +12,20 @@ import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog/doc
templateUrl: './document-type-list.component.html',
styleUrls: ['./document-type-list.component.scss']
})
export class DocumentTypeListComponent extends GenericListComponent<PaperlessDocumentType> implements OnInit {
export class DocumentTypeListComponent extends GenericListComponent<PaperlessDocumentType> {
constructor(service: DocumentTypeService, modalService: NgbModal, private titleService: Title) {
constructor(service: DocumentTypeService, modalService: NgbModal,
private list: DocumentListViewService
) {
super(service, modalService, DocumentTypeEditDialogComponent)
}
getObjectName(object: PaperlessDocumentType) {
return `document type '${object.name}'`
getDeleteMessage(object: PaperlessDocumentType) {
return $localize`Do you really want to delete the document type "${object.name}"?`
}
ngOnInit(): void {
super.ngOnInit()
this.titleService.setTitle(`Document types - ${environment.appTitle}`)
filterDocuments(object: PaperlessDocumentType) {
this.list.quickFilter([{rule_type: FILTER_DOCUMENT_TYPE, value: object.id.toString()}])
}
}

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