Compare commits

...
Author SHA1 Message Date
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
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
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
jonaswinkler 665863e395 Display name of current user on the dashboard 2020-12-20 17:18:23 +01: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
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
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
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
jonaswinkler 37237dfcf6 default title 2020-12-19 12:46:11 +01: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
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 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 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 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
jonaswinkler 48e785f7b6 Merge branch 'dev' 2020-12-10 02:24:58 +01:00
jonaswinkler 0cc22017de revert last commit. 2020-12-10 02:24:36 +01:00
jonaswinkler 3584f732a7 added another library that's required to get this running on raspberry pi 2020-12-10 02:14:26 +01:00
jonaswinkler 476beacd7f changelog 2020-12-10 01:12:30 +01:00
jonaswinkler 69c6d68219 a print() command somehow sneaked past my commit checks. 2020-12-10 00:59:03 +01:00
jonaswinkler 24d8a50f01 fixed an issue with the docker entrypoint script. 2020-12-10 00:54:37 +01:00
jonaswinkler 2df1894683 changelog 2020-12-10 00:30:35 +01:00
jonaswinkler 3f03cbf66c excluded the lockfile from the sanity checker. 2020-12-10 00:29:47 +01:00
jonaswinkler b3daf0efc3 added progress bar to the document renamer. 2020-12-10 00:10:36 +01:00
jonaswinkler 46c0ab943f added a progress bar to the reindex command. 2020-12-10 00:02:45 +01:00
jonaswinkler 2b57b80656 fixes #113 2020-12-09 23:45:53 +01:00
jonaswinkler 0b1b9de3cc layout fix 2020-12-09 22:38:52 +01:00
jonaswinkler 20c46278dc removed a janky test case that caused other test cases to fail 2020-12-09 22:18:03 +01:00
jonaswinkler 70cbdbf23b locking media directory while deleting files 2020-12-09 22:17:23 +01:00
jonaswinkler 6003122b06 fixes #112 2020-12-09 22:16:57 +01:00
jonaswinkler 8ca97924be shadows 2020-12-09 13:44:37 +01:00
jonaswinkler 2be0ba9f72 fixed test case. fixed bug with the decryption logic. 2020-12-09 13:27:02 +01: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 0a0d462938 tags from folders: case insensitive 2020-12-09 00:07:22 +01:00
jonaswinkler 74a99cf330 removed slugs entirely, since their only purpose was purely cosmetic anyway. 2020-12-09 00:04:37 +01:00
jonaswinkler 5753c83618 version bump 2020-12-08 21:20:05 +01:00
jonaswinkler 550a74347c a test that "verifies" that the file renaming lock works and no inconsistencies are created. 2020-12-08 21:08:44 +01:00
jonaswinkler e428a8a008 file upload improvements 2020-12-08 17:35:51 +01:00
jonaswinkler 001ab88fff docs 2020-12-08 17:34:38 +01:00
jonaswinkler d3cf85b9e9 Added a section on best practices. 2020-12-08 17:34:29 +01:00
jonaswinkler 91c722feff Merge branch 'master' into dev 2020-12-08 16:46:13 +01:00
jonaswinkler 871e22e3a3 documentation 2020-12-08 16:45:22 +01:00
jonaswinkler bf3b2249c5 Metadata documentation 2020-12-08 16:36:14 +01:00
jonaswinkler 6613104b4f date and time in metadata 2020-12-08 16:21:38 +01:00
jonaswinkler 0028fde2fd more metadata #32 2020-12-08 16:09:47 +01:00
jonaswinkler ad527fe97c reading and displaying PDF metadata 2020-12-08 15:45:02 +01:00
jonaswinkler 9da11f29c7 fixes #90 2020-12-08 13:54:49 +01:00
jonaswinkler c240fa1883 changelog 2020-12-08 11:53:58 +01:00
jonaswinkler bb33ac5e9e fixees #77 2020-12-08 01:12:03 +01:00
jonaswinkler 30f200ad39 fix z-order on the edit page. 2020-12-08 00:45:23 +01:00
jonaswinkler 5321ff1f20 upload status addresses #100 2020-12-08 00:45:11 +01:00
jonaswinkler c4a939dbcc addresses #104 2020-12-08 00:09:36 +01:00
jonaswinkler 3f05fe45bb Addresses #99 entirely. 2020-12-07 23:42:18 +01:00
jonaswinkler dfd844124d addresses #107 2020-12-07 22:29:51 +01:00
jonaswinkler d4febbc40f codestyle 2020-12-07 22:17:47 +01:00
jonaswinkler dc36e8566a addresses #106 2020-12-07 22:15:56 +01:00
jonaswinkler 87fa118de0 added filenames to the API #108 2020-12-07 21:52:26 +01:00
jonaswinkler 35c3d5c0b5 Merge branch 'dev' of github.com:jonaswinkler/paperless-ng into dev 2020-12-07 15:27:36 +01:00
jonaswinkler 9e46afafd7 fixes #102 2020-12-07 15:25:06 +01:00
jonaswinkler 56acd4f320 fixes #105 2020-12-07 12:46:46 +01:00
Jonas Winkler 2bbeb8ffe0 Update CONTRIBUTING.md 2020-12-06 23:30:51 +01:00
Jonas Winkler 5e188c0203 Merge pull request #97 from trahflow/dev
Dockerfile: Add libqpdf-dev to build dependencies
2020-12-06 23:25:34 +01:00
Wolfhart Feldmeier 8b637214b4 Dockerfile: Add libqpdf-dev to build dependencies 2020-12-06 23:00:20 +01:00
jonaswinkler eede5595e9 better error messages for file uploads. adresses #91 2020-12-06 22:31:12 +01:00
jonaswinkler c1fc8b2dac codestyle 2020-12-06 19:04:32 +01:00
jonaswinkler 28622d700d changed the way public filenames (i.e., for download and exporting) are generated. #94 2020-12-06 19:03:45 +01:00
jonaswinkler a079c310b4 changes to filename generation, partially addresses #90 2020-12-06 16:13:37 +01:00
jonaswinkler 278f6da16a documentation. 2020-12-06 14:41:14 +01: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 65816a434c Update README.md 2020-12-05 15:55:25 +01:00
jonaswinkler e3104d34fa Update README.md 2020-12-05 15:40:51 +01:00
jonaswinkler 891bd2de7f Update README.md 2020-12-05 15:37:23 +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
192 changed files with 5258 additions and 2034 deletions

No files matched your search

+4
View File
@@ -24,3 +24,7 @@ feature-X branches is for experimental stuff that will eventually be merged into
I'm trying to get most of paperless tested, so please do the same for your code! I know its a hassle, but it makes sure that your code works now and will allow us to detect regressions easily.
To test your code, execute `pytest` in the src/ directory. Executing that in the project root is no good. This also generates a html coverage report, which you can use to see if you missed anything important during testing.
## More info:
... is available in the documentation. https://paperless-ng.readthedocs.io/en/latest/extending.html
+2
View File
@@ -19,6 +19,7 @@ django-extensions = "*"
django-filter = "~=2.4.0"
django-q = "~=1.3.4"
djangorestframework = "~=3.12.2"
filelock = "*"
fuzzywuzzy = "*"
gunicorn = "*"
imap-tools = "*"
@@ -26,6 +27,7 @@ langdetect = "*"
pdftotext = "*"
pathvalidate = "*"
pillow = "*"
pikepdf = "*"
python-gnupg = "*"
python-dotenv = "*"
python-dateutil = "*"
Generated
+24 -15
View File
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "b10db53eb22d917723aa6107ff0970dc4e2aa886ee03d3ae08a994a856d57986"
"sha256": "3d576f289958226a7583e4c471c7f8c11bff6933bf093185f623cfb381a92412"
},
"pipfile-spec": 6,
"requires": {
@@ -197,6 +197,14 @@
"index": "pypi",
"version": "==3.12.2"
},
"filelock": {
"hashes": [
"sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59",
"sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"
],
"index": "pypi",
"version": "==3.0.12"
},
"fuzzywuzzy": {
"hashes": [
"sha256:45016e92264780e58972dca1b3d939ac864b78437422beecebb3095f8efd00e8",
@@ -425,7 +433,7 @@
"sha256:fe0ca120e3347c851c34a91041d574f3c588d832023906d8ae18d66d042e8a52",
"sha256:fe8e0152672f24d8bfdecc725f97e9013f2de1b41849150959526ca3562bd3ef"
],
"markers": "python_version < '3.9'",
"index": "pypi",
"version": "==2.2.0"
},
"pillow": {
@@ -858,10 +866,10 @@
},
"certifi": {
"hashes": [
"sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd",
"sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4"
"sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
"sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
],
"version": "==2020.11.8"
"version": "==2020.12.5"
},
"chardet": {
"hashes": [
@@ -961,17 +969,18 @@
},
"faker": {
"hashes": [
"sha256:7bca5b074299ac6532be2f72979e6793f1a2403ca8105cb4cf0b385a964469c4",
"sha256:fb21a76064847561033d8cab1cfd11af436ddf2c6fe72eb51b3cda51dff86bdc"
"sha256:1fcb415562ee6e2395b041e85fa6901d4708d30b84d54015226fa754ed0822c3",
"sha256:e8beccb398ee9b8cc1a91d9295121d66512b6753b4846eb1e7370545d46b3311"
],
"markers": "python_version >= '3.5'",
"version": "==5.0.0"
"markers": "python_version >= '3.6'",
"version": "==5.0.1"
},
"filelock": {
"hashes": [
"sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59",
"sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"
],
"index": "pypi",
"version": "==3.0.12"
},
"idna": {
@@ -1100,11 +1109,11 @@
},
"pygments": {
"hashes": [
"sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0",
"sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773"
"sha256:ccf3acacf3782cbed4a989426012f1c535c9a90d3a7fc3f16d231b9372d2b716",
"sha256:f275b6c0909e5dafd2d6269a656aa90fa58ebf4a74f8fcf9053195d226b24a08"
],
"markers": "python_version >= '3.5'",
"version": "==2.7.2"
"version": "==2.7.3"
},
"pyparsing": {
"hashes": [
@@ -1313,11 +1322,11 @@
},
"virtualenv": {
"hashes": [
"sha256:07cff122e9d343140366055f31be4dcd61fd598c69d11cd33a9d9c8df4546dd7",
"sha256:e0aac7525e880a429764cefd3aaaff54afb5d9f25c82627563603f5d7de5a6e5"
"sha256:54b05fc737ea9c9ee9f8340f579e5da5b09fb64fd010ab5757eb90268616907c",
"sha256:b7a8ec323ee02fb2312f098b6b4c9de99559b462775bc8fe3627a73706603c1b"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.2.1"
"version": "==20.2.2"
},
"zipp": {
"hashes": [
+24 -13
View File
@@ -7,7 +7,7 @@
[Paperless](https://github.com/the-paperless-project/paperless) is an application by Daniel Quinn and others that indexes your scanned documents and allows you to easily search for documents and store metadata alongside your documents.
Paperless-ng is a fork of the original project, adding a new interface and many other changes under the hood. For a detailed list of changes, see below.
Paperless-ng is a fork of the original project, adding a new interface and many other changes under the hood. For a detailed list of changes, have a look at the changelog in the documentation.
This project is still in development and some things may not work as expected.
@@ -15,11 +15,13 @@ This project is still in development and some things may not work as expected.
Paperless does not control your scanner, it only helps you deal with what your scanner produces.
1. Buy a document scanner that can write to a place on your network. If you need some inspiration, have a look at the [scanner recommendations](https://paperless-ng.readthedocs.io/en/latest/scanners.html) page.
2. Set it up to "scan to FTP" or something similar. It should be able to push scanned images to a server without you having to do anything. Of course if your scanner doesn't know how to automatically upload the file somewhere, you can always do that manually. Paperless doesn't care how the documents get into its local consumption directory.
3. Have the target server run the Paperless consumption script to OCR the file and index it into a local database.
4. Use the web frontend to sift through the database and find what you want.
5. Download the PDF you need/want via the web interface and do whatever you like with it. You can even print it and send it as if it's the original. In most cases, no one will care or notice.
1. Buy a document scanner that can write to a place on your network. If you need some inspiration, have a look at the [scanner recommendations](https://paperless-ng.readthedocs.io/en/latest/scanners.html) page. Set it up to "scan to FTP" or something similar. It should be able to push scanned images to a server without you having to do anything. Of course if your scanner doesn't know how to automatically upload the file somewhere, you can always do that manually. Paperless doesn't care how the documents get into its local consumption directory.
- Alternatively, you can use any of the mobile scanning apps out there. We have an app that allows you to share documents with paperless, if you're on Android. See the section on affiliated projects.
2. Wait for paperless to process your files. OCR is expensive, and depending on the power of your machine, this might take a bit of time.
3. Use the web frontend to sift through the database and find what you want.
4. Download the PDF you need/want via the web interface and do whatever you like with it. You can even print it and send it as if it's the original. In most cases, no one will care or notice.
Here's what you get:
@@ -28,11 +30,15 @@ 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.
* 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.
@@ -48,16 +54,16 @@ For a complete list of changes from paperless, check out the [changelog](https:/
# Roadmap for 1.0
- **Bulk editing**. Add/remove metadata from multiple documents at once.
- Make the front end nice (except mobile).
- Test coverage at 90%.
- Store archived documents with an embedded OCR text layer, while keeping originals available. Making good progress in the `feature-ocrmypdf` branch.
- 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
@@ -66,6 +72,9 @@ These are things that I want to add to paperless eventually. They are sorted by
- 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.
- 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, .docx documents.
Apart from that, paperless is pretty much feature complete.
## On the chopping block.
@@ -77,7 +86,7 @@ The recommended way to deploy paperless is docker-compose. Don't clone the repos
Read the [documentation](https://paperless-ng.readthedocs.io/en/latest/setup.html#installation) on how to get started.
Alternatively, you can install the dependencies and setup apache and a database server yourself. The documenation has information about the individual components of paperless that you need to take care of.
Alternatively, you can install the dependencies and setup apache and a database server yourself. The documenation has a step by step guide on how to do it.
# Migrating to paperless-ng
@@ -101,13 +110,15 @@ If you want to implement something big: Please start a discussion about that in
Paperless has been around a while now, and people are starting to build stuff on top of it. If you're one of those people, we can add your project to this list:
* [Paperless App](https://github.com/bauerj/paperless_app): An Android/iOS app for Paperless. We're working on making this compatible.
* [Paperless App](https://github.com/bauerj/paperless_app): An Android/iOS app for Paperless. Updated to work with paperless-ng.
* [Paperless Share](https://github.com/qcasey/paperless_share). Share any files from your Android application with paperless. Very simple, but works with all of the mobile scanning apps out there that allow you to share scanned documents.
These projects also exist, but their status and compatibility with paperless-ng is unknown.
* [Paperless Desktop](https://github.com/thomasbrueggemann/paperless-desktop): A desktop UI for your Paperless installation. Runs on Mac, Linux, and Windows.
* [ansible-role-paperless](https://github.com/ovv/ansible-role-paperless): An easy way to get Paperless running via Ansible.
* [paperless-cli](https://github.com/stgarf/paperless-cli): A golang command line binary to interact with a Paperless instance.
Compatibility with Paperless-ng is unknown.
# Important Note
Document scanners are typically used to scan sensitive documents. Things like your social insurance number, tax records, invoices, etc. Everything is stored in the clear without encryption by default (it needs to be searchable, so if someone has ideas on how to do that on encrypted data, I'm all ears). This means that Paperless should never be run on an untrusted host. Instead, I recommend that if you do want to use it, run it locally on a server in your own home.
+7 -2
View File
@@ -25,6 +25,11 @@ wait_for_postgres() {
host="${PAPERLESS_DBHOST}"
port="${PAPERLESS_DBPORT}"
if [[ -z $port ]] ;
then
port="5432"
fi
while !</dev/tcp/$host/$port ;
do
@@ -114,13 +119,13 @@ install_languages() {
done
}
initialize
# Install additional languages if specified
if [[ ! -z "$PAPERLESS_OCR_LANGUAGES" ]]; then
install_languages "$PAPERLESS_OCR_LANGUAGES"
fi
initialize
if [[ "$1" != "/"* ]]; then
exec sudo -HEu paperless python3 manage.py "$@"
else
+1 -1
View File
@@ -15,7 +15,7 @@ services:
POSTGRES_PASSWORD: paperless
webserver:
image: jonaswinkler/paperless-ng:0.9.5
image: jonaswinkler/paperless-ng:0.9.9
restart: always
depends_on:
- db
+1 -1
View File
@@ -5,7 +5,7 @@ services:
restart: always
webserver:
image: jonaswinkler/paperless-ng:0.9.5
image: jonaswinkler/paperless-ng:0.9.9
restart: always
depends_on:
- broker
+2 -1
View File
@@ -18,6 +18,7 @@ RUN apt-get update \
libmagic-dev \
libpoppler-cpp-dev \
libpq-dev \
libqpdf-dev \
libxml2 \
optipng \
pngquant \
@@ -34,7 +35,7 @@ RUN apt-get update \
zlib1g \
&& pip3 install --upgrade supervisor setuptools \
&& pip install --no-cache-dir -r requirements.txt \
&& apt-get -y purge build-essential \
&& apt-get -y purge build-essential libqpdf-dev \
&& apt-get -y autoremove --purge \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir /var/log/supervisord /var/run/supervisord
+10 -7
View File
@@ -119,8 +119,11 @@ Updating paperless without docker
After grabbing the new release and unpacking the contents, do the following:
1. Update python requirements. Paperless uses
`Pipenv`_ for managing dependencies:
1. Update dependencies. New paperless version may require additional
dependencies. The dependencies required are listed in the section about
:ref:`bare metal installations <setup-bare_metal>`.
2. Update python requirements. If you use Pipenv, this is done with the following steps.
.. code:: shell-session
@@ -132,14 +135,14 @@ After grabbing the new release and unpacking the contents, do the following:
This creates a new virtual environment (or uses your existing environment)
and installs all dependencies into it.
2. Collect static files.
3. Collect static files.
.. code:: shell-session
$ cd src
$ pipenv run python3 manage.py collectstatic --clear
3. Migrate the database.
4. Migrate the database.
.. code:: shell-session
@@ -153,14 +156,14 @@ Management utilities
Paperless comes with some management commands that perform various maintenance
tasks on your paperless instance. You can invoke these commands either by
.. code:: bash
.. code:: shell-session
$ cd /path/to/paperless
$ docker-compose run --rm webserver <command> <arguments>
or
.. code:: bash
.. code:: shell-session
$ cd /path/to/paperless/src
$ pipenv run python manage.py <command> <arguments>
@@ -366,7 +369,7 @@ is specified, the archiver will only process that document.
.. note::
Some documents will cause errors and cannot be converted into PDF/A documents,
such as encrypted PDF documents. The archiver will skip over these Documents
such as encrypted PDF documents. The archiver will skip over these documents
each time it sees them.
.. _utilities-encyption:
+22 -95
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::
@@ -298,6 +218,8 @@ avoids filename clashes.
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.
@@ -307,10 +229,15 @@ Paperless provides the following placeholders withing filenames:
* ``{added_year}``: Year added only.
* ``{added_month}``: Month added only (number 1-12).
* ``{added_day}``: Day added only (number 1-31).
* ``{tags}``: I don't know how this works. Look at the source.
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::
+84 -27
View File
@@ -13,23 +13,55 @@ available filters and ordering fields.
The API provides 5 main endpoints:
* ``/api/documents/``: Full CRUD support, except POSTing new documents. See below.
* ``/api/correspondents/``: Full CRUD support.
* ``/api/document_types/``: Full CRUD support.
* ``/api/documents/``: Full CRUD support, except POSTing new documents. See below.
* ``/api/logs/``: Read-Only.
* ``/api/tags/``: Full CRUD support.
All of these endpoints except for the logging endpoint
All of these endpoints except for the logging endpoint
allow you to fetch, edit and delete individual objects
by appending their primary key to the path, for example ``/api/documents/454/``.
The objects served by the document endpoint contain the following fields:
* ``id``: ID of the document. Read-only.
* ``title``: Title of the document.
* ``content``: Plain text content of the document.
* ``tags``: List of IDs of tags assigned to this document, or empty list.
* ``document_type``: Document type of this document, or null.
* ``correspondent``: Correspondent of this document or null.
* ``created``: The date at which this document was created.
* ``modified``: The date at which this document was last edited in paperless. Read-only.
* ``added``: The date at which this document was added to paperless. Read-only.
* ``archive_serial_number``: The identifier of this document in a physical document archive.
* ``original_file_name``: Verbose filename of the original document. Read-only.
* ``archived_file_name``: Verbose filename of the archived document. Read-only. Null if no archived document is available.
Downloading documents
#####################
In addition to that, the document endpoint offers these additional actions on
individual documents:
* ``/api/documents/<pk>/download/``: Download the original document.
* ``/api/documents/<pk>/thumb/``: Download the PNG thumbnail of a document.
* ``/api/documents/<pk>/preview/``: Display the original document inline,
* ``/api/documents/<pk>/download/``: Download the document.
* ``/api/documents/<pk>/preview/``: Display the document inline,
without downloading it.
* ``/api/documents/<pk>/thumb/``: Download the PNG thumbnail of a document.
Paperless generates archived PDF/A documents from consumed files and stores both
the original files as well as the archived files. By default, the endpoints
for previews and downloads serve the archived file, if it is available.
Otherwise, the original file is served.
Some document cannot be archived.
The endpoints correctly serve the response header fields ``Content-Disposition``
and ``Content-Type`` to indicate the filename for download and the type of content of
the document.
In order to download or preview the original document when an archied document is available,
supply the query parameter ``original=true``.
.. hint::
@@ -38,13 +70,43 @@ individual documents:
are in place. However, if you use these old URLs to access documents, you
should update your app or script to use the new URLs.
.. note::
The document endpoint provides tags, document types and correspondents as
ids in their corresponding fields. These are writeable. Paperless also
offers read-only objects for assigned tags, types and correspondents,
however, these might be removed in the future. As for now, the front end
requires them.
Getting document metadata
#########################
The api also has an endpoint to retrieve read-only metadata about specific documents. this
information is not served along with the document objects, since it requires reading
files and would therefore slow down document lists considerably.
Access the metadata of a document with an ID ``id`` at ``/api/documents/<id>/metadata/``.
The endpoint reports the following data:
* ``original_checksum``: MD5 checksum of the original document.
* ``original_size``: Size of the original document, in bytes.
* ``original_mime_type``: Mime type of the original document.
* ``media_filename``: Current filename of the document, under which it is stored inside the media directory.
* ``has_archive_version``: True, if this document is archived, false otherwise.
* ``original_metadata``: A list of metadata associated with the original document. See below.
* ``archive_checksum``: MD5 checksum of the archived document, or null.
* ``archive_size``: Size of the archived document in bytes, or null.
* ``archive_metadata``: Metadata associated with the archived document, or null. See below.
File metadata is reported as a list of objects in the following form:
.. code:: json
[
{
"namespace": "http://ns.adobe.com/pdf/1.3/",
"prefix": "pdf",
"key": "Producer",
"value": "SparklePDF, Fancy edition"
},
]
``namespace`` and ``prefix`` can be null. The actual metadata reported depends on the file type and the metadata
available in that specific document. Paperless only reports PDF metadata at this point.
Authorization
#############
@@ -54,11 +116,11 @@ The REST api provides three different forms of authentication.
1. Basic authentication
Authorize by providing a HTTP header in the form
.. code::
Authorization: Basic <credentials>
where ``credentials`` is a base64-encoded string of ``<username>:<password>``
2. Session authentication
@@ -79,7 +141,7 @@ The REST api provides three different forms of authentication.
.. code::
Authorization: Token <token>
Tokens can be managed and revoked in the paperless admin.
Searching for documents
@@ -111,7 +173,7 @@ Result list object returned by the endpoint:
"page_count": 1,
"corrected_query": "",
"results": [
]
}
@@ -131,12 +193,12 @@ Result object:
{
"id": 1,
"highlights": [
],
"score": 6.34234,
"rank": 23,
"document": {
}
}
@@ -159,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. ...
+129
View File
@@ -5,6 +5,133 @@
Changelog
*********
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
##################
This release focusses primarily on many small issues with the UI.
* Front end
* Paperless now has proper window titles.
* Fixed an issue with the small cards when more than 7 tags were used.
* Navigation of the "Show all" links adjusted. They navigate to the saved view now, if available in the sidebar.
* Some indication on the document lists that a filter is active was added.
* There's a new filter to filter for documents that do *not* have a certain tag.
* The file upload box now shows upload progress.
* The document edit page was reorganized.
* The document edit page shows various information about a document.
* An issue with the height of the preview was fixed.
* Table issues with too long document titles fixed.
* API
* The API now serves file names with documents.
* The API now serves various metadata about documents.
* API documentation updated.
* Other
* Fixed an issue with the docker image when a non-standard PostgreSQL port was used.
* The docker image was trying check for installed languages before actually installing them.
* ``FILENAME_FORMAT`` placeholder for document types.
* The filename formatter is now less restrictive with file names and tries to
conserve the original correspondents, types and titles as much as possible.
* The filename formatter does not include the document ID in filenames anymore. It will
rather append ``_01``, ``_02``, etc when it detects duplicate filenames.
.. note::
The changes to the filename format will apply to newly added documents and changed documents.
If you want all files to reflect these changes, execute the ``document_renamer`` management
command.
paperless-ng 0.9.5
##################
@@ -800,6 +927,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
+10 -5
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,6 @@ 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.
Defaults to none, which disables this feature.
Binaries
########
+56 -90
View File
@@ -118,114 +118,80 @@ This will test and assemble everything and also build and tag a docker image.
Extending Paperless
===================
.. warning::
Paperless does not have any fancy plugin systems and will probably never have. However,
some parts of the application have been designed to allow easy integration of additional
features without any modification to the base code.
This section is not updated to paperless-ng yet.
Making custom parsers
---------------------
For the most part, Paperless is monolithic, so extending it is often best
managed by way of modifying the code directly and issuing a pull request on
`GitHub`_. However, over time the project has been evolving to be a little
more "pluggable" so that users can write their own stuff that talks to it.
Paperless uses parsers to add documents to paperless. A parser is responsible for:
.. _GitHub: https://github.com/the-paperless-project/paperless
* Retrieve the content from the original
* Create a thumbnail
* Optional: Retrieve a created date from the original
* Optional: Create an archived document from the original
Custom parsers can be added to paperless to support more file types. In order to do that,
you need to write the parser itself and announce its existence to paperless.
.. _extending-parsers:
Parsers
-------
You can leverage Paperless' consumption model to have it consume files *other*
than ones handled by default like ``.pdf``, ``.jpg``, and ``.tiff``. To do so,
you simply follow Django's convention of creating a new app, with a few key
requirements.
.. _extending-parsers-parserspy:
parsers.py
..........
In this file, you create a class that extends
``documents.parsers.DocumentParser`` and go about implementing the three
required methods:
* ``get_thumbnail()``: Returns the path to a file we can use as a thumbnail for
this document.
* ``get_text()``: Returns the text from the document and only the text.
* ``get_date()``: If possible, this returns the date of the document, otherwise
it should return ``None``.
.. _extending-parsers-signalspy:
signals.py
..........
At consumption time, Paperless emits a ``document_consumer_declaration``
signal which your module has to react to in order to let the consumer know
whether or not it's capable of handling a particular file. Think of it like
this:
1. Consumer finds a file in the consumption directory.
2. It asks all the available parsers: *"Hey, can you handle this file?"*
3. Each parser responds with either ``None`` meaning they can't handle the
file, or a dictionary in the following format:
The parser itself must extend ``documents.parsers.DocumentParser`` and must implement the
methods ``parse`` and ``get_thumbnail``. You can provide your own implementation to
``get_date`` if you don't want to rely on paperless' default date guessing mechanisms.
.. code:: python
{
"parser": <the class name>,
"weight": <an integer>
}
class MyCustomParser(DocumentParser):
The consumer compares the ``weight`` values from all respondents and uses the
class with the highest value to consume the document. The default parser,
``RasterisedDocumentParser`` has a weight of ``0``.
def parse(self, document_path, mime_type):
# This method does not return anything. Rather, you should assign
# whatever you got from the document to the following fields:
# The content of the document.
self.text = "content"
# Optional: path to a PDF document that you created from the original.
self.archive_path = os.path.join(self.tempdir, "archived.pdf")
.. _extending-parsers-appspy:
# Optional: "created" date of the document.
self.date = get_created_from_metadata(document_path)
apps.py
.......
def get_thumbnail(self, document_path, mime_type):
# This should return the path to a thumbnail you created for this
# document.
return os.path.join(self.tempdir, "thumb.png")
This is a standard Django file, but you'll need to add some code to it to
connect your parser to the ``document_consumer_declaration`` signal.
If you encounter any issues during parsing, raise a ``documents.parsers.ParseError``.
The ``self.tempdir`` directory is a temporary directory that is guaranteed to be empty
and removed after consumption finished. You can use that directory to store any
intermediate files and also use it to store the thumbnail / archived document.
.. _extending-parsers-finally:
Finally
.......
The last step is to update ``settings.py`` to include your new module.
Eventually, this will be dynamic, but at the moment, you have to edit the
``INSTALLED_APPS`` section manually. Simply add the path to your AppConfig to
the list like this:
After that, you need to announce your parser to paperless. You need to connect a
handler to the ``document_consumer_declaration`` signal. Have a look in the file
``src/paperless_tesseract/apps.py`` on how that's done. The handler is a method
that returns information about your parser:
.. code:: python
INSTALLED_APPS = [
...
"my_module.apps.MyModuleConfig",
...
]
def myparser_consumer_declaration(sender, **kwargs):
return {
"parser": MyCustomParser,
"weight": 0,
"mime_types": {
"application/pdf": ".pdf",
"image/jpeg": ".jpg",
}
}
Order doesn't matter, but generally it's a good idea to place your module lower
in the list so that you don't end up accidentally overriding project defaults
somewhere.
* ``parser`` is a reference to a class that extends ``DocumentParser``.
* ``weight`` is used whenever two or more parsers are able to parse a file: The parser with
the higher weight wins. This can be used to override the parsers provided by
paperless.
.. _extending-parsers-example:
An Example
..........
The core Paperless functionality is based on this design, so if you want to see
what a parser module should look like, have a look at `parsers.py`_,
`signals.py`_, and `apps.py`_ in the `paperless_tesseract`_ module.
.. _parsers.py: https://github.com/the-paperless-project/paperless/blob/master/src/paperless_tesseract/parsers.py
.. _signals.py: https://github.com/the-paperless-project/paperless/blob/master/src/paperless_tesseract/signals.py
.. _apps.py: https://github.com/the-paperless-project/paperless/blob/master/src/paperless_tesseract/apps.py
.. _paperless_tesseract: https://github.com/the-paperless-project/paperless/blob/master/src/paperless_tesseract/
* ``mime_types`` is a dictionary. The keys are the mime types your parser supports and the value
is the default file extension that paperless should use when storing files and serving them for
download. We could guess that from the file extensions, but some mime types have many extensions
associated with them and the python methods responsible for guessing the extension do not always
return the same value.
+7 -1
View File
@@ -73,11 +73,17 @@ in your browser and paperless has to do much less work to serve the data.
**Q:** *How do I install paperless-ng on Raspberry Pi?*
**A:** There is not docker image for ARM available. If you know how to build
**A:** There is no docker image for ARM available. If you know how to build
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
+11
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
============
@@ -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:
+27 -1
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.
+75 -5
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 three options: the consumption directory, IMAP (email), and
HTTP POST.
When adding documents to paperless, it will perform the following operations on
your documents:
@@ -82,8 +79,7 @@ your documents:
No matter which options you choose, Paperless will always store the original
document that it found in the consumption directory or in the mail and
will never overwrite that document. Archived versions are stored alongside the
digital versions.
original versions.
The consumption directory
@@ -107,6 +103,23 @@ files from the scanner. Typically, you're looking at an FTP server like
.. TODO: hyperref to configuration of the location of this magic folder.
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)
@@ -183,6 +196,63 @@ You can also submit a document using the REST API, see :ref:`api-file_uploads` f
.. _basic-searching:
Best practices
##############
Paperless offers a couple tools that help you organize your document collection. However,
it is up to you to use them in a way that helps you organize documents and find specific
documents when you need them. This section offers a couple ideas for managing your collection.
Document types allow you to classify documents according to what they are. You can define
types such as "Receipt", "Invoice", or "Contract". If you used to collect all your receipts
in a single binder, you can recreate that system in paperless by defining a document type,
assigning documents to that type and then filtering by that type to only see all receipts.
Not all documents need document types. Sometimes its hard to determine what the type of a
document is or it is hard to justify creating a document type that you only need once or twice.
This is okay. As long as the types you define help you organize your collection in the way
you want, paperless is doing its job.
Tags can be used in many different ways. Think of tags are more versatile folders or binders.
If you have a binder for documents related to university / your car or health care, you can
create these binders in paperless by creating tags and assigning them to relevant documents.
Just as with documents, you can filter the document list by tags and only see documents of
a certain topic.
With physical documents, you'll often need to decide which folder the document belongs to.
The advantage of tags over folders and binders is that a single document can have multiple
tags. A physical document cannot magically appear in two different folders, but with tags,
this is entirely possible.
.. hint::
This can be used in many different ways. One example: Imagine you're working on a particular
task, such as signing up for university. Usually you'll need to collect a bunch of different
documents that are already sorted into various folders. With the tag system of paperless,
you can create a new group of documents that are relevant to this task without destroying
the already existing organization. When you're done with the task, you could delete the
tag again, which would be equal to sorting documents back into the folder they belong into.
Or keep the tag, up to you.
All of the logic above applies to correspondents as well. Attach them to documents if you
feel that they help you organize your collection.
When you've started organizing your documents, create a couple saved views for document collections
you regularly access. This is equal to having labeled physical binders on your desk, except
that these saved views are dynamic and simply update themselves as you add documents to the system.
Here are a couple examples of tags and types that you could use in your collection.
* An ``inbox`` tag for newly added documents that you haven't manually edited yet.
* A tag ``car`` for everything car related (repairs, registration, insurance, etc)
* A tag ``todo`` for documents that you still need to do something with, such as reply, or
perform some task online.
* A tag ``bank account x`` for all bank statement related to that account.
* A tag ``mail`` for anything that you added to paperless via its mail processing capabilities.
* A tag ``missing_metadata`` when you still need to add some metadata to a document, but can't
or don't want to do this right now.
Searching
#########
+1
View File
@@ -30,6 +30,7 @@
#PAPERLESS_FORCE_SCRIPT_NAME=
#PAPERLESS_STATIC_URL=/static/
#PAPERLESS_AUTO_LOGIN_USERNAME=
#PAPERLESS_COOKIE_PREFIX=
# OCR settings
+1
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:
+69 -2
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",
+2
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",
+29 -7
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';
@@ -28,6 +27,9 @@ import { PageHeaderComponent } from './components/common/page-header/page-header
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 { FilterDropdownComponent } from './components/filter-editor/filter-dropdown/filter-dropdown.component';
import { FilterDropdownButtonComponent } from './components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component';
import { FilterDropdownDateComponent } from './components/filter-editor/filter-dropdown-date/filter-dropdown-date.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 { NgxFileDropModule } from 'ngx-file-drop';
@@ -45,7 +47,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 { NgSelectModule } from '@ng-select/ng-select';
import { SelectDialogComponent } from './components/common/select-dialog/select-dialog.component';
@NgModule({
declarations: [
@@ -58,10 +68,9 @@ import { WelcomeWidgetComponent } from './components/dashboard/widgets/welcome-w
DocumentTypeListComponent,
LogsComponent,
SettingsComponent,
SafePipe,
NotFoundComponent,
CorrespondentEditDialogComponent,
DeleteDialogComponent,
ConfirmDialogComponent,
TagEditDialogComponent,
DocumentTypeEditDialogComponent,
TagComponent,
@@ -71,6 +80,9 @@ import { WelcomeWidgetComponent } from './components/dashboard/widgets/welcome-w
AppFrameComponent,
ToastsComponent,
FilterEditorComponent,
FilterDropdownComponent,
FilterDropdownButtonComponent,
FilterDropdownDateComponent,
DocumentCardLargeComponent,
DocumentCardSmallComponent,
TextComponent,
@@ -84,7 +96,13 @@ import { WelcomeWidgetComponent } from './components/dashboard/widgets/welcome-w
StatisticsWidgetComponent,
UploadFileWidgetComponent,
WidgetFrameComponent,
WelcomeWidgetComponent
WelcomeWidgetComponent,
YesNoPipe,
FileSizePipe,
FilterPipe,
DocumentTitlePipe,
MetadataCollapseComponent,
SelectDialogComponent
],
imports: [
BrowserModule,
@@ -94,7 +112,9 @@ import { WelcomeWidgetComponent } from './components/dashboard/widgets/welcome-w
FormsModule,
ReactiveFormsModule,
NgxFileDropModule,
InfiniteScrollModule
InfiniteScrollModule,
PdfViewerModule,
NgSelectModule
],
providers: [
DatePipe,
@@ -102,7 +122,9 @@ import { WelcomeWidgetComponent } from './components/dashboard/widgets/welcome-w
provide: HTTP_INTERCEPTORS,
useClass: CsrfInterceptor,
multi: true
}
},
FilterPipe,
DocumentTitlePipe
],
bootstrap: [AppComponent]
})
@@ -17,6 +17,11 @@
<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">
@@ -37,16 +42,16 @@
</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'>
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted" *ngIf='savedViewService.sidebarViews.length > 0'>
<span>Saved views</span>
</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}}
{{view.name}}
</a>
</li>
</ul>
@@ -60,7 +65,7 @@
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
</svg>
{{d.title}}
{{d.title | documentTitle}}
</a>
</li>
<li class="nav-item w-100" *ngIf="openDocuments.length > 1">
@@ -132,7 +137,7 @@
</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>
@@ -140,7 +145,7 @@
</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>
@@ -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() {
@@ -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>
@@ -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();
});
});
@@ -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 = "Confirmation"
@Input()
messageBold
@Input()
message
@Input()
btnClass = "btn-primary"
@Input()
btnCaption = "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()
}
}
@@ -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()
}
}
@@ -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()
@@ -3,11 +3,10 @@
<label for="created_date">{{titleDate}}</label>
<input type="date" class="form-control" id="created_date" [(ngModel)]="dateValue" (change)="dateOrTimeChanged()">
</div>
<div class="form-group col">
<div class="form-group col" *ngIf="titleTime">
<label for="created_time">{{titleTime}}</label>
<input type="time" class="form-control" id="created_time" [(ngModel)]="timeValue" (change)="dateOrTimeChanged()">
</div>
</div>
@@ -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: [{
@@ -40,7 +39,7 @@ export class DateTimeComponent implements OnInit,ControlValueAccessor {
titleDate: string = "Date"
@Input()
titleTime: string = "Time"
titleTime: string
@Input()
disabled: boolean = false
@@ -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>
@@ -0,0 +1 @@
// styles for ng-select child are in styles.scss
@@ -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';
@@ -1,30 +1,41 @@
<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"
[disabled]="disabled"
(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">
<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>
@@ -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;
}
@@ -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)
}
}
@@ -1,6 +1,5 @@
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { v4 as uuidv4 } from 'uuid';
import { Component, forwardRef } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { AbstractInputComponent } from '../abstract-input';
@Component({
@@ -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 {
}
}
@@ -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()">Cancel</button>
<button type="button" class="btn btn-primary" (click)="selectClicked.emit(selected)">Select</button>
</div>
@@ -1,20 +1,20 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DeleteDialogComponent } from './delete-dialog.component';
import { SelectDialogComponent } from './select-dialog.component';
describe('DeleteDialogComponent', () => {
let component: DeleteDialogComponent;
let fixture: ComponentFixture<DeleteDialogComponent>;
describe('SelectDialogComponent', () => {
let component: SelectDialogComponent;
let fixture: ComponentFixture<SelectDialogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ DeleteDialogComponent ]
declarations: [ SelectDialogComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DeleteDialogComponent);
fixture = TestBed.createComponent(SelectDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
@@ -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 = "Select"
@Input()
message = "Please select an object"
@Input()
objects: ObjectWithId[] = []
selected: number
ngOnInit(): void {
}
cancelClicked() {
this.activeModal.close()
}
}
@@ -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({
@@ -1,4 +1,4 @@
<app-page-header title="Dashboard" subTitle="Welcome to paperless-ng!">
<app-page-header title="Dashboard" [subTitle]="subtitle">
<img src="assets/logo.svg" height="80" class="m-2 d-none d-md-block">
</app-page-header>
@@ -1,5 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { SavedViewConfigService } from 'src/app/services/saved-view-config.service';
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({
@@ -10,13 +12,36 @@ import { SavedViewConfigService } from 'src/app/services/saved-view-config.servi
export class DashboardComponent implements OnInit {
constructor(
public savedViewConfigService: SavedViewConfigService) { }
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 `Hello ${this.displayName}, welcome to Paperless-ng!`
} else {
return `Welcome to Paperless-ng!`
}
}
savedViews: PaperlessSavedView[] = []
ngOnInit(): void {
this.savedViews = this.savedViewConfigService.getDashboardConfigs()
this.savedViewService.listAll().subscribe(results => {
this.savedViews = results.results.filter(savedView => savedView.show_on_dashboard)
})
}
}
@@ -1,4 +1,4 @@
<app-widget-frame [title]="savedView.title">
<app-widget-frame [title]="savedView.name">
<a header-buttons [routerLink]="" (click)="showAll()">Show all</a>
@@ -13,7 +13,7 @@
<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>
@@ -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,19 +18,23 @@ 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() {
this.list.load(this.savedView)
this.router.navigate(["documents"])
if (this.savedView.show_in_sidebar) {
this.router.navigate(['view', this.savedView.id])
} else {
this.list.load(this.savedView)
this.router.navigate(["documents"])
}
}
}
@@ -1,15 +1,18 @@
<app-widget-frame title="Upload new documents">
<form content>
<ngx-file-drop
dropZoneLabel="Drop documents here or" (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">
<div content>
<form>
<ngx-file-drop dropZoneLabel="Drop documents here or" (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">
</ngx-file-drop>
</form>
</ngx-file-drop>
</form>
<div *ngIf="uploadVisible" class="mt-3">
<p>Uploading {{uploadStatus.length}} file(s)</p>
<ngb-progressbar [value]="loadedSum" [max]="totalSum" [striped]="true" [animated]="uploadStatus.length > 0">
</ngb-progressbar>
</div>
</div>
</app-widget-frame>
@@ -1,8 +1,15 @@
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';
interface UploadStatus {
loaded: number
total: number
}
@Component({
selector: 'app-upload-file-widget',
templateUrl: './upload-file-widget.component.html',
@@ -16,26 +23,59 @@ export class UploadFileWidgetComponent implements OnInit {
}
public fileOver(event){
console.log(event);
}
public fileLeave(event){
console.log(event);
}
uploadStatus: UploadStatus[] = []
completedFiles = 0
uploadVisible = false
get loadedSum() {
return this.uploadStatus.map(s => s.loaded).reduce((a,b) => a+b, this.completedFiles > 0 ? 1 : 0)
}
get totalSum() {
return this.uploadStatus.map(s => s.total).reduce((a,b) => a+b, 1)
}
public dropped(files: NgxFileDropEntry[]) {
for (const droppedFile of files) {
if (droppedFile.fileEntry.isFile) {
const fileEntry = droppedFile.fileEntry as FileSystemFileEntry;
console.log(fileEntry)
let uploadStatusObject: UploadStatus = {loaded: 0, total: 1}
this.uploadStatus.push(uploadStatusObject)
this.uploadVisible = true
const fileEntry = droppedFile.fileEntry as FileSystemFileEntry;
fileEntry.file((file: File) => {
console.log(file)
const formData = new FormData()
let formData = new FormData()
formData.append('document', file, file.name)
this.documentService.uploadDocument(formData).subscribe(result => {
this.toastService.showToast(Toast.make("Information", "The document has been uploaded and will be processed by the consumer shortly."))
this.documentService.uploadDocument(formData).subscribe(event => {
if (event.type == HttpEventType.UploadProgress) {
uploadStatusObject.loaded = event.loaded
uploadStatusObject.total = event.total
} 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."))
}
}, error => {
this.toastService.showToast(Toast.makeError("An error has occured while uploading the document. Sorry!"))
this.uploadStatus.splice(this.uploadStatus.indexOf(uploadStatusObject), 1)
this.completedFiles += 1
switch (error.status) {
case 400: {
this.toastService.showToast(Toast.makeError(`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!"))
break;
}
}
})
});
}
@@ -1,4 +1,4 @@
<div class="card mb-3 shadow">
<div class="card mb-3 shadow-sm">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">{{title}}</h5>
@@ -1,4 +1,14 @@
<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">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">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" />
@@ -14,16 +24,22 @@
</svg>
<span class="d-none d-lg-inline"> Download</span>
</a>
<div class="btn-group" ngbDropdown role="group" *ngIf="metadata?.paperless__has_archive_version">
<button class="btn btn-sm btn-outline-primary dropdown-toggle-split" ngbDropdownToggle></button>
<div class="dropdown-menu" ngbDropdownMenu>
<a ngbDropdownItem [href]="downloadOriginalUrl">Download original</a>
</div>
</div>
</div>
<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>
</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>
<span class="d-none d-lg-inline"> More like this</span>
</button>
<button type="button" class="btn btn-sm btn-outline-primary" (click)="close()">
<svg class="buttonicon" fill="currentColor">
@@ -35,41 +51,100 @@
<div class="row">
<div class="col-xl">
<div class="col mb-4">
<form [formGroup]='documentForm' (ngSubmit)="save()">
<app-input-text title="Title" formControlName="title"></app-input-text>
<ul ngbNav #nav="ngbNav" class="nav-tabs">
<li [ngbNavItem]="1">
<a ngbNavLink>Details</a>
<ng-template ngbNavContent>
<div class="form-group">
<label for="archive_serial_number">Archive Serial Number</label>
<input type="number" class="form-control" id="archive_serial_number"
formControlName='archive_serial_number'>
</div>
<app-input-text title="Title" formControlName="title"></app-input-text>
<div class="form-group">
<label for="archive_serial_number">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 title="Date created" titleTime="Time created" formControlName="created"></app-input-date-time>
</ng-template>
</li>
<div class="form-group">
<label for="content">Content</label>
<textarea class="form-control" id="content" rows="5" formControlName='content'></textarea>
</div>
<li [ngbNavItem]="2">
<a ngbNavLink>Content</a>
<ng-template ngbNavContent>
<div class="form-group">
<textarea class="form-control" id="content" rows="20" formControlName='content'></textarea>
</div>
</ng-template>
</li>
<app-input-select [items]="correspondents" title="Correspondent" formControlName="correspondent" allowNull="true" (createNew)="createCorrespondent()"></app-input-select>
<li [ngbNavItem]="3">
<a ngbNavLink>Metadata</a>
<ng-template ngbNavContent>
<app-input-select [items]="documentTypes" title="Document type" formControlName="document_type" allowNull="true" (createNew)="createDocumentType()"></app-input-select>
<table class="table table-borderless">
<tbody>
<tr>
<td>Date modified</td>
<td>{{document.modified | date:'medium'}}</td>
</tr>
<tr>
<td>Date added</td>
<td>{{document.added | date:'medium'}}</td>
</tr>
<tr>
<td>Media filename</td>
<td>{{metadata?.media_filename}}</td>
</tr>
<tr>
<td>Original MD5 Checksum</td>
<td>{{metadata?.original_checksum}}</td>
</tr>
<tr>
<td>Original file size</td>
<td>{{metadata?.original_size | fileSize}}</td>
</tr>
<tr>
<td>Original mime type</td>
<td>{{metadata?.original_mime_type}}</td>
</tr>
<tr *ngIf="metadata?.has_archive_version">
<td>Archive MD5 Checksum</td>
<td>{{metadata?.archive_checksum}}</td>
</tr>
<tr *ngIf="metadata?.has_archive_version">
<td>Archive file size</td>
<td>{{metadata?.archive_size | fileSize}}</td>
</tr>
</tbody>
</table>
<app-input-tags formControlName="tags" title="Tags"></app-input-tags>
<app-metadata-collapse title="Original document metadata" [metadata]="metadata.original_metadata" *ngIf="metadata?.original_metadata?.length > 0"></app-metadata-collapse>
<app-metadata-collapse title="Archived document metadata" [metadata]="metadata.archive_metadata" *ngIf="metadata?.archive_metadata?.length > 0"></app-metadata-collapse>
</ng-template>
</li>
</ul>
<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="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;
</form>
</div>
<div class="col-xl">
<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" (after-load-complete)="pdfPreviewLoaded($event)"></pdf-viewer>
</div>
</div>
</div>
@@ -0,0 +1,6 @@
.pdf-viewer-container {
height: calc(100vh - 160px);
top: 70px;
position: sticky;
background-color: gray;
}
@@ -6,14 +6,16 @@ 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 { 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',
@@ -22,6 +24,9 @@ import { DocumentTypeEditDialogComponent } from '../manage/document-type-list/do
})
export class DocumentDetailComponent implements OnInit {
public expandOriginalMetadata = false;
public expandArchivedMetadata = false;
documentId: number
document: PaperlessDocument
metadata: PaperlessDocumentMetadata
@@ -43,15 +48,23 @@ 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,
private router: Router,
private modalService: NgbModal,
private openDocumentService: OpenDocumentsService,
private documentListViewService: DocumentListViewService) { }
private documentListViewService: DocumentListViewService,
private documentTitlePipe: DocumentTitlePipe) { }
getContentType() {
return this.metadata?.has_archive_version ? 'application/pdf' : this.metadata?.original_mime_type
}
ngOnInit(): void {
this.documentForm.valueChanges.subscribe(wow => {
@@ -83,7 +96,7 @@ export class DocumentDetailComponent implements OnInit {
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)
}
@@ -117,7 +130,7 @@ export class DocumentDetailComponent implements OnInit {
}, error => {this.router.navigate(['404'])})
}
save() {
save() {
this.documentsService.update(this.document).subscribe(result => {
this.close()
})
@@ -144,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 = "Confirm delete"
modal.componentInstance.messageBold = `Do you really want to delete document '${this.document.title}'?`
modal.componentInstance.message = `The files for this document will be deleted permanently. This operation cannot be undone.`
modal.componentInstance.btnClass = "btn-danger"
modal.componentInstance.btnCaption = "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
}
}
@@ -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>
@@ -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();
});
});
@@ -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 = "Metadata"
ngOnInit(): void {
}
}
@@ -1,18 +1,26 @@
<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)="selected = 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)="selected = $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>
<ng-template #nolink>{{(document.correspondent$ | async)?.name}}</ng-template>:
</ng-container>
{{document.title}}
{{document.title | documentTitle}}
<app-tag [tag]="t" linkTitle="Filter by tag" *ngFor="let t of document.tags$ | async" class="ml-1" (click)="clickTag.emit(t.id)" [clickable]="clickTag.observers.length"></app-tag>
</h5>
<h5 class="card-title" *ngIf="document.archive_serial_number">#{{document.archive_serial_number}}</h5>
@@ -23,8 +31,14 @@
</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>
More like this
</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"/>
@@ -45,11 +59,17 @@
</svg>
Download
</a>
</div>
<small class="text-muted ml-auto">Score:</small>
<ngb-progressbar *ngIf="searchScore" [type]="searchScoreClass" [value]="searchScore" class="search-score-bar mx-2" [max]="1"></ngb-progressbar>
<small class="text-muted">Created: {{document.created | date}}</small>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -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;
}
@@ -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,28 @@ export class DocumentCardLargeComponent implements OnInit {
constructor(private documentService: DocumentService, private sanitizer: DomSanitizer) { }
_selected = false
get selected() {
return this._selected
}
@Input()
set selected(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 +46,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 {
}
@@ -1,22 +1,36 @@
<div class="col p-2 h-100" style="width: 16rem;">
<div class="card h-100 shadow-sm">
<div class=" border-bottom doc-img pr-1" [ngStyle]="{'background-image': 'url(' + getThumbUrl() + ')'}">
<div class="row" *ngFor="let t of document.tags$ | async">
<app-tag style="font-size: large;" [tag]="t" class="col text-right" (click)="clickTag.emit(t.id)" [clickable]="true" linkTitle="Filter by tag"></app-tag>
<div class="col p-2 h-100 document-card">
<div class="card h-100 shadow-sm" [class.card-selected]="selected">
<div class="border-bottom" [class.doc-img-background-selected]="selected">
<img class="card-img doc-img" [src]="getThumbUrl()" (click)="selected = !selected">
<div style="top: 0; left: 0" class="position-absolute border-right border-bottom bg-light p-1" [class.document-card-check]="!selected">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="smallCardCheck{{document.id}}" [checked]="selected" (change)="selected = $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>
</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>:
</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">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
@@ -36,9 +50,9 @@
</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>
@@ -1,5 +1,24 @@
@import "/src/theme";
.doc-img {
background-size: cover;
background-position: top;
object-fit: cover;
object-position: top;
height: 200px;
mix-blend-mode: multiply;
}
.document-card-check {
display: none
}
.document-card:hover .document-card-check {
display: block;
}
.card-selected {
border-color: $primary;
}
.doc-img-background-selected {
background-color: $primaryFaded;
}
@@ -1,6 +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({
@@ -12,6 +12,21 @@ export class DocumentCardSmallComponent implements OnInit {
constructor(private documentService: DocumentService) { }
_selected = false
get selected() {
return this._selected
}
@Input()
set selected(value: boolean) {
this._selected = value
this.selectedChange.emit(value)
}
@Output()
selectedChange = new EventEmitter<boolean>()
@Input()
document: PaperlessDocument
@@ -21,6 +36,8 @@ export class DocumentCardSmallComponent implements OnInit {
@Output()
clickCorrespondent = new EventEmitter<number>()
moreTags: number = null
ngOnInit(): void {
}
@@ -35,4 +52,18 @@ export class DocumentCardSmallComponent implements OnInit {
getPreviewUrl() {
return this.documentService.getPreviewUrl(this.document.id)
}
getTagsLimited$() {
return this.document.tags$.pipe(
map(tags => {
if (tags.length > 7) {
this.moreTags = tags.length - 6
return tags.slice(0, 6)
} else {
return tags
}
})
)
}
}
@@ -1,5 +1,28 @@
<app-page-header [title]="getTitle()">
<div ngbDropdown class="d-inline-block mr-2">
<button class="btn btn-sm btn-outline-primary" id="dropdownBasic1" ngbDropdownToggle>
<svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#text-indent-left" />
</svg>
Bulk edit
</button>
<div ngbDropdownMenu aria-labelledby="dropdownBasic1" class="shadow">
<button ngbDropdownItem (click)="list.selectPage()">Select page</button>
<button ngbDropdownItem (click)="list.selectAll()">Select all</button>
<button ngbDropdownItem (click)="list.selectNone()">Select none</button>
<div class="dropdown-divider"></div>
<button ngbDropdownItem [disabled]="list.selected.size == 0" (click)="bulkSetCorrespondent()">Set correspondent</button>
<button ngbDropdownItem [disabled]="list.selected.size == 0" (click)="bulkRemoveCorrespondent()">Remove correspondent</button>
<button ngbDropdownItem [disabled]="list.selected.size == 0" (click)="bulkSetDocumentType()">Set document type</button>
<button ngbDropdownItem [disabled]="list.selected.size == 0" (click)="bulkRemoveDocumentType()">Remove document type</button>
<button ngbDropdownItem [disabled]="list.selected.size == 0" (click)="bulkAddTag()">Add tag</button>
<button ngbDropdownItem [disabled]="list.selected.size == 0" (click)="bulkRemoveTag()">Remove tag</button>
<div class="dropdown-divider"></div>
<button ngbDropdownItem [disabled]="list.selected.size == 0" (click)="bulkDelete()">Delete</button>
</div>
</div>
<div class="btn-group btn-group-toggle" ngbRadioGroup [(ngModel)]="displayMode"
(ngModelChange)="saveDisplayMode()">
<label ngbButtonLabel class="btn-outline-primary btn-sm">
@@ -21,72 +44,66 @@
</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>
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
<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 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>
<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>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>
</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 [(filterRules)]="list.filterRules" #filterEditor></app-filter-editor>
</div>
<div class="d-flex justify-content-between align-items-center">
<p>{{list.collectionSize || 0}} document(s)</p>
<p><span *ngIf="list.selected.size > 0">Selected {{list.selected.size}} of </span>{{list.collectionSize || 0}} document(s) <span *ngIf="isFiltered">(filtered)</span></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" *ngIf="displayMode == 'details'">
<table class="table table-sm border shadow-sm" *ngIf="displayMode == 'details'">
<thead>
<th></th>
<th class="d-none d-lg-table-cell">ASN</th>
<th class="d-none d-md-table-cell">Correspondent</th>
<th>Title</th>
@@ -95,22 +112,28 @@
<th class="d-none d-xl-table-cell">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">{{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>
@@ -124,6 +147,6 @@
</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>
@@ -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;
}
}
}
}
@@ -1,14 +1,22 @@
import { Component, OnInit } from '@angular/core';
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 { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
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 { CorrespondentService } from 'src/app/services/rest/correspondent.service';
import { DocumentTypeService } from 'src/app/services/rest/document-type.service';
import { DocumentService, DOCUMENT_SORT_FIELDS } from 'src/app/services/rest/document.service';
import { TagService } from 'src/app/services/rest/tag.service';
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
import { Toast, ToastService } from 'src/app/services/toast.service';
import { FilterEditorComponent } from '../filter-editor/filter-editor.component';
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component';
import { SelectDialogComponent } from '../common/select-dialog/select-dialog.component';
import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component';
import { OpenDocumentsService } from 'src/app/services/open-documents.service';
@Component({
selector: 'app-document-list',
@@ -19,15 +27,25 @@ 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) { }
public modalService: NgbModal,
private correspondentService: CorrespondentService,
private documentTypeService: DocumentTypeService,
private tagService: TagService,
private documentService: DocumentService,
private openDocumentService: OpenDocumentsService) { }
@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"
@@ -46,91 +64,184 @@ 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.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.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.showToast(Toast.make("Information", `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.showToast(Toast.make("Information", `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.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.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.filterEditor.toggleDocumentType(documentTypeID)
}
trackByDocumentId(index, item: PaperlessDocument) {
return item.id
}
private executeBulkOperation(method: string, args): Observable<any> {
return this.documentService.bulkEdit(Array.from(this.list.selected), method, args).pipe(
tap(() => {
this.list.reload()
this.list.selected.forEach(id => {
this.openDocumentService.refreshDocument(id)
})
this.list.selectNone()
})
)
}
bulkSetCorrespondent() {
let modal = this.modalService.open(SelectDialogComponent, {backdrop: 'static'})
modal.componentInstance.title = "Select correspondent"
modal.componentInstance.message = `Select the correspondent you wish to assign to ${this.list.selected.size} selected document(s):`
this.correspondentService.listAll().subscribe(response => {
modal.componentInstance.objects = response.results
})
modal.componentInstance.selectClicked.subscribe(selectedId => {
this.executeBulkOperation('set_correspondent', {"correspondent": selectedId}).subscribe(
response => {
modal.close()
}
)
})
}
bulkRemoveCorrespondent() {
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
modal.componentInstance.title = "Remove correspondent"
modal.componentInstance.message = `This operation will remove the correspondent from all ${this.list.selected.size} selected document(s).`
modal.componentInstance.confirmClicked.subscribe(() => {
this.executeBulkOperation('set_correspondent', {"correspondent": null}).subscribe(r => {
modal.close()
})
})
}
bulkSetDocumentType() {
let modal = this.modalService.open(SelectDialogComponent, {backdrop: 'static'})
modal.componentInstance.title = "Select document type"
modal.componentInstance.message = `Select the document type you wish to assign to ${this.list.selected.size} selected document(s):`
this.documentTypeService.listAll().subscribe(response => {
modal.componentInstance.objects = response.results
})
modal.componentInstance.selectClicked.subscribe(selectedId => {
this.executeBulkOperation('set_document_type', {"document_type": selectedId}).subscribe(
response => {
modal.close()
}
)
})
}
bulkRemoveDocumentType() {
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
modal.componentInstance.title = "Remove document type"
modal.componentInstance.message = `This operation will remove the document type from all ${this.list.selected.size} selected document(s).`
modal.componentInstance.confirmClicked.subscribe(() => {
this.executeBulkOperation('set_document_type', {"document_type": null}).subscribe(r => {
modal.close()
})
})
}
bulkAddTag() {
let modal = this.modalService.open(SelectDialogComponent, {backdrop: 'static'})
modal.componentInstance.title = "Select tag"
modal.componentInstance.message = `Select the tag you wish to assign to ${this.list.selected.size} selected document(s):`
this.tagService.listAll().subscribe(response => {
modal.componentInstance.objects = response.results
})
modal.componentInstance.selectClicked.subscribe(selectedId => {
this.executeBulkOperation('add_tag', {"tag": selectedId}).subscribe(
response => {
modal.close()
}
)
})
}
bulkRemoveTag() {
let modal = this.modalService.open(SelectDialogComponent, {backdrop: 'static'})
modal.componentInstance.title = "Select tag"
modal.componentInstance.message = `Select the tag you wish to remove from ${this.list.selected.size} selected document(s):`
this.tagService.listAll().subscribe(response => {
modal.componentInstance.objects = response.results
})
modal.componentInstance.selectClicked.subscribe(selectedId => {
this.executeBulkOperation('remove_tag', {"tag": selectedId}).subscribe(
response => {
modal.close()
}
)
})
}
bulkDelete() {
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
modal.componentInstance.delayConfirm(5)
modal.componentInstance.title = "Delete confirm"
modal.componentInstance.messageBold = `This operation will permanently delete all ${this.list.selected.size} selected document(s).`
modal.componentInstance.message = `This operation cannot be undone.`
modal.componentInstance.btnClass = "btn-danger"
modal.componentInstance.btnCaption = "Delete document(s)"
modal.componentInstance.confirmClicked.subscribe(() => {
this.executeBulkOperation("delete", {}).subscribe(
response => {
modal.close()
}
)
})
}
}
@@ -6,9 +6,9 @@
</button>
</div>
<div class="modal-body">
<app-input-text title="Title" formControlName="title"></app-input-text>
<app-input-text title="Name" formControlName="name"></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-check 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>
@@ -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 {
@@ -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-filter 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>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>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>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>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>
@@ -0,0 +1,7 @@
.date-filter {
min-width: 250px;
.btn-link {
line-height: 1;
}
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FilterDropdownDateComponent } from './filter-dropdown-date.component';
describe('FilterDropdownDateComponent', () => {
let component: FilterDropdownDateComponent;
let fixture: ComponentFixture<FilterDropdownDateComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ FilterDropdownDateComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FilterDropdownDateComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,103 @@
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 FILTER_LAST_7_DAYS = 0
const FILTER_LAST_MONTH = 1
const FILTER_LAST_3_MONTHS = 2
const FILTER_LAST_YEAR = 3
@Component({
selector: 'app-filter-dropdown-date',
templateUrl: './filter-dropdown-date.component.html',
styleUrls: ['./filter-dropdown-date.component.scss']
})
export class FilterDropdownDateComponent implements OnInit, OnDestroy {
quickFilters = [
{id: FILTER_LAST_7_DAYS, name: "Last 7 days"},
{id: FILTER_LAST_MONTH, name: "Last month"},
{id: FILTER_LAST_3_MONTHS, name: "Last 3 months"},
{id: FILTER_LAST_YEAR, name: "Last year"}
]
@Input()
dateBefore: string
@Input()
dateAfter: 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 FILTER_LAST_7_DAYS:
date.setDate(date.getDate() - 7)
break;
case FILTER_LAST_MONTH:
date.setMonth(date.getMonth() - 1)
break;
case FILTER_LAST_3_MONTHS:
date.setMonth(date.getMonth() - 3)
break
case FILTER_LAST_YEAR:
date.setFullYear(date.getFullYear() - 1)
break
}
this.dateAfter = formatDate(date, 'yyyy-MM-dd', "en-us", "UTC")
this.onChange()
}
onChange() {
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()
}
}
@@ -0,0 +1,12 @@
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-left-0 border-right-0 border-bottom" role="menuitem" (click)="toggleItem()">
<div class="selected-icon mr-1">
<svg *ngIf="selected" width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-check" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M10.97 4.97a.75.75 0 0 1 1.071 1.05l-3.992 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.236.236 0 0 1 .02-.022z"/>
</svg>
</div>
<div class="mr-1">
<app-tag *ngIf="isTag; else displayName" [tag]="item" [clickable]="true" linkTitle="Filter by tag"></app-tag>
<ng-template #displayName><small>{{item.name}}</small></ng-template>
</div>
<div class="badge badge-light rounded-pill ml-auto mr-1">{{item.document_count}}</div>
</button>
@@ -0,0 +1,4 @@
.selected-icon {
min-width: 1em;
min-height: 1em;
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FilterDropodownButtonComponent } from './filter-dropdown-button.component';
describe('FilterDropodownButtonComponent', () => {
let component: FilterDropodownButtonComponent;
let fixture: ComponentFixture<FilterDropodownButtonComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ FilterDropodownButtonComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FilterDropodownButtonComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,32 @@
import { Component, EventEmitter, Input, Output, OnInit } from '@angular/core';
import { PaperlessTag } from 'src/app/data/paperless-tag';
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
@Component({
selector: 'app-filter-dropdown-button',
templateUrl: './filter-dropdown-button.component.html',
styleUrls: ['./filter-dropdown-button.component.scss']
})
export class FilterDropdownButtonComponent implements OnInit {
@Input()
item: PaperlessTag | PaperlessDocumentType | PaperlessCorrespondent
@Input()
selected: boolean
@Output()
toggle = new EventEmitter()
isTag: boolean
ngOnInit() {
this.isTag = 'is_inbox_tag' in this.item // ~ this.item instanceof PaperlessTag
}
toggleItem(): void {
this.selected = !this.selected
this.toggle.emit(this.item)
}
}
@@ -0,0 +1,29 @@
<div class="btn-group" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #filterDropdown="ngbDropdown">
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="itemsSelected?.length > 0 ? 'btn-primary' : 'btn-outline-primary'">
<div class="d-none d-md-inline">{{title}}</div>
<div class="d-inline-block d-md-none">
<svg class="toolbaricon" fill="currentColor">
<use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
</svg>
</div>
<ng-container *ngIf="itemsSelected?.length > 0">
<div class="badge bg-secondary text-light rounded-pill badge-corner">
{{itemsSelected?.length}}
</div>
</ng-container>
</button>
<div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
<div class="list-group list-group-flush">
<div class="list-group-item">
<div class="input-group input-group-sm">
<input class="form-control" type="text" [(ngModel)]="filterText" placeholder="Filter {{title}}" (keyup.enter)="listFilterEnter()" #listFilterTextInput>
</div>
</div>
<div *ngIf="items" class="items">
<ng-container *ngFor="let item of items | filter: filterText; let i = index">
<app-filter-dropdown-button [item]="item" [selected]="isItemSelected(item)" (toggle)="toggleItem($event)"></app-filter-dropdown-button>
</ng-container>
</div>
</div>
</div>
</div>
@@ -0,0 +1,14 @@
.badge-corner {
position: absolute;
top: -8px;
right: -8px;
}
.dropdown-menu {
min-width: 250px;
.items {
max-height: 400px;
overflow-y: scroll;
}
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FilterDropodownComponent } from './filter-dropdown.component';
describe('FilterDropodownComponent', () => {
let component: FilterDropodownComponent;
let fixture: ComponentFixture<FilterDropodownComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ FilterDropodownComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FilterDropodownComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,58 @@
import { Component, EventEmitter, Input, Output, ElementRef, ViewChild } from '@angular/core';
import { ObjectWithId } from 'src/app/data/object-with-id';
import { FilterPipe } from 'src/app/pipes/filter.pipe';
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
@Component({
selector: 'app-filter-dropdown',
templateUrl: './filter-dropdown.component.html',
styleUrls: ['./filter-dropdown.component.scss']
})
export class FilterDropdownComponent {
constructor(private filterPipe: FilterPipe) { }
@Input()
items: ObjectWithId[]
@Input()
itemsSelected: ObjectWithId[]
@Input()
title: string
@Input()
icon: string
@Output()
toggle = new EventEmitter()
@ViewChild('listFilterTextInput') listFilterTextInput: ElementRef
@ViewChild('filterDropdown') filterDropdown: NgbDropdown
filterText: string
toggleItem(item: ObjectWithId): void {
this.toggle.emit(item)
}
isItemSelected(item: ObjectWithId): boolean {
return this.itemsSelected?.find(i => i.id == item.id) !== undefined
}
dropdownOpenChange(open: boolean): void {
if (open) {
setTimeout(() => {
this.listFilterTextInput.nativeElement.focus();
}, 0);
} else {
this.filterText = ''
}
}
listFilterEnter(): void {
let filtered = this.filterPipe.transform(this.items, this.filterText)
if (filtered.length == 1) this.toggleItem(filtered.shift())
this.filterDropdown.close()
}
}
@@ -1,52 +1,27 @@
<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 class="row">
<div class="col mb-2 mb-xl-0">
<div class="form-inline d-flex">
<label class="text-muted mr-2">Filter by:</label>
<input class="form-control form-control-sm flex-grow-1" type="text" [(ngModel)]="titleFilter" placeholder="Title">
</div>
</div>
<div class="w-100 d-xl-none"></div>
<div class="col col-xl-auto mb-2 mb-xl-0">
<div class="d-flex">
<app-filter-dropdown class="mr-2 mr-md-3" [items]="tags" [itemsSelected]="selectedTags" title="Tags" icon="tag-fill" (toggle)="toggleTag($event.id)"></app-filter-dropdown>
<app-filter-dropdown class="mr-2 mr-md-3" [items]="correspondents" [itemsSelected]="selectedCorrespondents" title="Correspondents" icon="person-fill" (toggle)="toggleCorrespondent($event.id)"></app-filter-dropdown>
<app-filter-dropdown class="mr-2 mr-md-3" [items]="documentTypes" [itemsSelected]="selectedDocumentTypes" title="Document types" icon="file-earmark-fill" (toggle)="toggleDocumentType($event.id)"></app-filter-dropdown>
<app-filter-dropdown-date class="mr-2 mr-md-3" [dateBefore]="dateCreatedBefore" [dateAfter]="dateCreatedAfter" title="Created" (datesSet)="onDatesCreatedSet($event)"></app-filter-dropdown-date>
<app-filter-dropdown-date [dateBefore]="dateAddedBefore" [dateAfter]="dateAddedAfter" title="Added" (datesSet)="onDatesAddedSet($event)"></app-filter-dropdown-date>
</div>
</div>
<div class="w-100 d-xl-none"></div>
<div class="col col-xl-auto mb-2 mb-xl-0">
<button class="btn btn-link btn-sm px-0 mx-0 ml-xl-n4" [disabled]="!hasFilters()" (click)="clearSelected()">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
</svg>
Clear all filters
</button>
</div>
</div>
@@ -0,0 +1,10 @@
.quick-filter {
min-width: 250px;
max-height: 400px;
overflow-y: scroll;
.selected-icon {
min-width: 1em;
min-height: 1em;
}
}
@@ -1,67 +1,239 @@
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 { 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 { PaperlessTag } from 'src/app/data/paperless-tag';
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
import { Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { NgbDateParserFormatter, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap';
import { DocumentTypeService } from 'src/app/services/rest/document-type.service';
import { TagService } from 'src/app/services/rest/tag.service';
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
import { FilterRule } from 'src/app/data/filter-rule';
import { FILTER_ADDED_AFTER, FILTER_ADDED_BEFORE, FILTER_CORRESPONDENT, FILTER_CREATED_AFTER, FILTER_CREATED_BEFORE, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_RULE_TYPES, FILTER_TITLE } from 'src/app/data/filter-rule-type';
import { DateSelection } from './filter-dropdown-date/filter-dropdown-date.component';
@Component({
selector: 'app-filter-editor',
templateUrl: './filter-editor.component.html',
styleUrls: ['./filter-editor.component.scss']
})
export class FilterEditorComponent implements OnInit {
export class FilterEditorComponent implements OnInit, OnDestroy {
constructor(private documentTypeService: DocumentTypeService, private tagService: TagService, private correspondentService: CorrespondentService) { }
generateFilterName() {
if (this.filterRules.length == 1) {
let rule = this.filterRules[0]
switch(this.filterRules[0].rule_type) {
case FILTER_CORRESPONDENT:
return `Correspondent: ${this.correspondents.find(c => c.id == +rule.value)?.name}`
@Output()
clear = new EventEmitter()
case FILTER_DOCUMENT_TYPE:
return `Type: ${this.documentTypes.find(dt => dt.id == +rule.value)?.name}`
@Input()
filterRules: FilterRule[] = []
case FILTER_HAS_TAG:
return `Tag: ${this.tags.find(t => t.id == +rule.value)?.name}`
@Output()
apply = new EventEmitter()
}
}
selectedRuleType: FilterRuleType = FILTER_RULE_TYPES[0]
correspondents: PaperlessCorrespondent[] = []
tags: PaperlessTag[] = []
documentTypes: PaperlessDocumentType[] = []
newRuleClicked() {
this.filterRules.push({type: this.selectedRuleType, value: null})
this.selectedRuleType = this.getRuleTypes().length > 0 ? this.getRuleTypes()[0] : null
return ""
}
removeRuleClicked(rule) {
let index = this.filterRules.findIndex(r => r == rule)
if (index > -1) {
this.filterRules.splice(index, 1)
constructor(
private documentTypeService: DocumentTypeService,
private tagService: TagService,
private correspondentService: CorrespondentService,
private dateParser: NgbDateParserFormatter
) { }
tags: PaperlessTag[] = []
correspondents: PaperlessCorrespondent[]
documentTypes: PaperlessDocumentType[] = []
@Input()
filterRules: FilterRule[]
@Output()
filterRulesChange = new EventEmitter<FilterRule[]>()
hasFilters() {
return this.filterRules.length > 0
}
get selectedTags(): PaperlessTag[] {
let tagRules: FilterRule[] = this.filterRules.filter(fr => fr.rule_type == FILTER_HAS_TAG)
return this.tags?.filter(t => tagRules.find(tr => +tr.value == t.id))
}
get selectedCorrespondents(): PaperlessCorrespondent[] {
let correspondentRules: FilterRule[] = this.filterRules.filter(fr => fr.rule_type == FILTER_CORRESPONDENT)
return this.correspondents?.filter(c => correspondentRules.find(cr => +cr.value == c.id))
}
get selectedDocumentTypes(): PaperlessDocumentType[] {
let documentTypeRules: FilterRule[] = this.filterRules.filter(fr => fr.rule_type == FILTER_DOCUMENT_TYPE)
return this.documentTypes?.filter(dt => documentTypeRules.find(dtr => +dtr.value == dt.id))
}
get titleFilter() {
let existingRule = this.filterRules.find(rule => rule.rule_type == FILTER_TITLE)
return existingRule ? existingRule.value : ''
}
set titleFilter(value) {
this.titleFilterDebounce.next(value)
}
titleFilterDebounce: Subject<string>
subscription: Subscription
ngOnInit() {
this.tagService.listAll().subscribe(result => this.tags = result.results)
this.correspondentService.listAll().subscribe(result => this.correspondents = result.results)
this.documentTypeService.listAll().subscribe(result => this.documentTypes = result.results)
this.titleFilterDebounce = new Subject<string>()
this.subscription = this.titleFilterDebounce.pipe(
debounceTime(400),
distinctUntilChanged()
).subscribe(title => {
this.setTitleRule(title)
})
}
ngOnDestroy() {
this.titleFilterDebounce.complete()
// TODO: not sure if both is necessary
this.subscription.unsubscribe()
}
applyFilters() {
this.filterRulesChange.next(this.filterRules)
}
clearSelected() {
this.filterRules = []
this.applyFilters()
}
private toggleFilterRule(filterRuleTypeID: number, value: number) {
let filterRuleType = FILTER_RULE_TYPES.find(t => t.id == filterRuleTypeID)
let existingRule = this.filterRules.find(rule => rule.rule_type == filterRuleTypeID && rule.value == value?.toString())
let existingRuleOfSameType = this.filterRules.find(rule => rule.rule_type == filterRuleTypeID)
if (existingRule) {
// if this exact rule already exists, remove it in all cases.
this.filterRules.splice(this.filterRules.indexOf(existingRule), 1)
} else if (filterRuleType.multi || !existingRuleOfSameType) {
// if we allow multiple rules per type, or no rule of this type already exists, push a new rule.
this.filterRules.push({rule_type: filterRuleTypeID, value: value?.toString()})
} else {
// otherwise (i.e., no multi support AND there's already a rule of this type), update the rule.
existingRuleOfSameType.value = value?.toString()
}
this.applyFilters()
}
private setTitleRule(title: string) {
let existingRule = this.filterRules.find(rule => rule.rule_type == FILTER_TITLE)
if (!existingRule && title) {
this.filterRules.push({rule_type: FILTER_TITLE, value: title})
} else if (existingRule && !title) {
this.filterRules.splice(this.filterRules.findIndex(rule => rule.rule_type == FILTER_TITLE), 1)
} else if (existingRule && title) {
existingRule.value = title
}
this.applyFilters()
}
toggleTag(tagId: number) {
this.toggleFilterRule(FILTER_HAS_TAG, tagId)
}
toggleCorrespondent(correspondentId: number) {
this.toggleFilterRule(FILTER_CORRESPONDENT, correspondentId)
}
toggleDocumentType(documentTypeId: number) {
this.toggleFilterRule(FILTER_DOCUMENT_TYPE, documentTypeId)
}
// Date handling
onDatesCreatedSet(dates: DateSelection) {
this.setDateCreatedBefore(dates.before)
this.setDateCreatedAfter(dates.after)
this.applyFilters()
}
onDatesAddedSet(dates: DateSelection) {
this.setDateAddedBefore(dates.before)
this.setDateAddedAfter(dates.after)
this.applyFilters()
}
get dateCreatedBefore(): string {
let createdBeforeRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_CREATED_BEFORE)
return createdBeforeRule ? createdBeforeRule.value : null
}
get dateCreatedAfter(): string {
let createdAfterRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_CREATED_AFTER)
return createdAfterRule ? createdAfterRule.value : null
}
get dateAddedBefore(): string {
let addedBeforeRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_ADDED_BEFORE)
return addedBeforeRule ? addedBeforeRule.value : null
}
get dateAddedAfter(): string {
let addedAfterRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_ADDED_AFTER)
return addedAfterRule ? addedAfterRule.value : null
}
setDateCreatedBefore(date?: string) {
if (date) this.setDateFilter(date, FILTER_CREATED_BEFORE)
else this.clearDateFilter(FILTER_CREATED_BEFORE)
}
setDateCreatedAfter(date?: string) {
if (date) this.setDateFilter(date, FILTER_CREATED_AFTER)
else this.clearDateFilter(FILTER_CREATED_AFTER)
}
setDateAddedBefore(date?: string) {
if (date) this.setDateFilter(date, FILTER_ADDED_BEFORE)
else this.clearDateFilter(FILTER_ADDED_BEFORE)
}
setDateAddedAfter(date?: string) {
if (date) this.setDateFilter(date, FILTER_ADDED_AFTER)
else this.clearDateFilter(FILTER_ADDED_AFTER)
}
setDateFilter(date: string, dateRuleTypeID: number) {
let existingRule = this.filterRules.find(rule => rule.rule_type == dateRuleTypeID)
if (existingRule) {
existingRule.value = date
} else {
this.filterRules.push({rule_type: dateRuleTypeID, value: date})
}
}
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))
clearDateFilter(dateRuleTypeID: number) {
let ruleIndex = this.filterRules.findIndex(rule => rule.rule_type == dateRuleTypeID)
if (ruleIndex != -1) {
this.filterRules.splice(ruleIndex, 1)
}
}
}
@@ -8,10 +8,9 @@
<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 title="Match" formControlName="match" hint="Auto matching does not require you to fill in this field."></app-input-text>
<app-input-check title="Case insensitive" formControlName="is_insensitive" hint="Auto matching ignores this option."></app-input-check>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button>
@@ -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';
@@ -26,9 +26,26 @@
<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>
Documents
</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>
Edit
</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>
Delete
</button>
</div>
</td>
</tr>
</tbody>
@@ -1,6 +1,9 @@
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
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 { GenericListComponent } from '../generic-list/generic-list.component';
import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog/correspondent-edit-dialog.component';
@@ -12,12 +15,21 @@ import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog/co
})
export class CorrespondentListComponent extends GenericListComponent<PaperlessCorrespondent> {
constructor(correspondentsService: CorrespondentService,
modalService: NgbModal) {
super(correspondentsService,modalService,CorrespondentEditDialogComponent)
}
constructor(correspondentsService: CorrespondentService, modalService: NgbModal,
private router: Router,
private list: DocumentListViewService
) {
super(correspondentsService,modalService,CorrespondentEditDialogComponent)
}
getObjectName(object: PaperlessCorrespondent) {
return `correspondent '${object.name}'`
}
getObjectName(object: PaperlessCorrespondent) {
return `correspondent '${object.name}'`
}
filterDocuments(object: PaperlessCorrespondent) {
this.list.documentListView.filter_rules = [
{rule_type: FILTER_CORRESPONDENT, value: object.id.toString()}
]
this.router.navigate(["documents"])
}
}
@@ -8,9 +8,9 @@
<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 title="Match" formControlName="match" hint="Auto matching does not require you to fill in this field."></app-input-text>
<app-input-check title="Case insensitive" formControlName="is_insensitive" hint="Auto matching ignores this option."></app-input-check>
</div>
<div class="modal-footer">
@@ -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';
@@ -25,8 +25,25 @@
<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>
Documents
</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>
Edit
</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>
Delete
</button>
</div>
</td>
</tr>
@@ -1,6 +1,9 @@
import { Component, OnInit } from '@angular/core';
import { Component } from '@angular/core';
import { Router } from '@angular/router';
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 { GenericListComponent } from '../generic-list/generic-list.component';
import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog/document-type-edit-dialog.component';
@@ -12,11 +15,21 @@ import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog/doc
})
export class DocumentTypeListComponent extends GenericListComponent<PaperlessDocumentType> {
constructor(service: DocumentTypeService, modalService: NgbModal) {
constructor(service: DocumentTypeService, modalService: NgbModal,
private router: Router,
private list: DocumentListViewService
) {
super(service, modalService, DocumentTypeEditDialogComponent)
}
}
getObjectName(object: PaperlessDocumentType) {
getObjectName(object: PaperlessDocumentType) {
return `document type '${object.name}'`
}
filterDocuments(object: PaperlessDocumentType) {
this.list.documentListView.filter_rules = [
{rule_type: FILTER_DOCUMENT_TYPE, value: object.id.toString()}
]
this.router.navigate(["documents"])
}
}
@@ -4,13 +4,13 @@ import { MatchingModel, MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/mat
import { ObjectWithId } from 'src/app/data/object-with-id';
import { SortableDirective, SortEvent } from 'src/app/directives/sortable.directive';
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service';
import { DeleteDialogComponent } from '../../common/delete-dialog/delete-dialog.component';
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component';
@Directive()
export abstract class GenericListComponent<T extends ObjectWithId> implements OnInit {
constructor(
private service: AbstractPaperlessService<T>,
private service: AbstractPaperlessService<T>,
private modalService: NgbModal,
private editDialogComponent: any) {
}
@@ -60,7 +60,8 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On
}
reloadData() {
this.service.list(this.page, null, this.sortField, this.sortDirection).subscribe(c => {
// TODO: this is a hack
this.service.list(this.page, null, this.sortField, this.sortDirection == 'des').subscribe(c => {
this.data = c.results
this.collectionSize = c.count
});
@@ -88,10 +89,13 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On
}
openDeleteDialog(object: T) {
var activeModal = this.modalService.open(DeleteDialogComponent, {backdrop: 'static'})
activeModal.componentInstance.message = `Do you really want to delete ${this.getObjectName(object)}?`
activeModal.componentInstance.message2 = "Associated documents will not be deleted."
activeModal.componentInstance.deleteClicked.subscribe(() => {
var activeModal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
activeModal.componentInstance.title = "Confirm delete"
activeModal.componentInstance.messageBold = `Do you really want to delete ${this.getObjectName(object)}?`
activeModal.componentInstance.message = "Associated documents will not be deleted."
activeModal.componentInstance.btnClass = "btn-danger"
activeModal.componentInstance.btnCaption = "Delete"
activeModal.componentInstance.confirmClicked.subscribe(() => {
this.service.delete(object).subscribe(_ => {
activeModal.close()
this.reloadData()
@@ -1,5 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { kMaxLength } from 'buffer';
import { LOG_LEVELS, LOG_LEVEL_INFO, PaperlessLog } from 'src/app/data/paperless-log';
import { LogService } from 'src/app/services/rest/log.service';
@@ -20,7 +19,7 @@ export class LogsComponent implements OnInit {
}
reload() {
this.logService.list(1, 50, 'created', 'des', {'level__gte': this.level}).subscribe(result => this.logs = result.results)
this.logService.list(1, 50, 'created', true, {'level__gte': this.level}).subscribe(result => this.logs = result.results)
}
getLevelText(level: number) {
@@ -32,7 +31,7 @@ export class LogsComponent implements OnInit {
if (this.logs.length > 0) {
lastCreated = new Date(this.logs[this.logs.length-1].created).toISOString()
}
this.logService.list(1, 25, 'created', 'des', {'created__lt': lastCreated, 'level__gte': this.level}).subscribe(result => {
this.logService.list(1, 25, 'created', true, {'created__lt': lastCreated, 'level__gte': this.level}).subscribe(result => {
this.logs.push(...result.results)
})
}
@@ -34,24 +34,35 @@
<a ngbNavLink>Saved views</a>
<ng-template ngbNavContent>
<table class="table table-borderless table-sm">
<thead>
<tr>
<th scope="col">Title</th>
<th scope="col">Show in dashboard</th>
<th scope="col">Show in sidebar</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let config of savedViewConfigService.getConfigs()">
<td>{{ config.title }}</td>
<td>{{ config.showInDashboard }}</td>
<td>{{ config.showInSideBar }}</td>
<td><button type="button" class="btn btn-sm btn-outline-danger" (click)="deleteViewConfig(config)">Delete</button></td>
</tr>
</tbody>
</table>
<div formGroupName="savedViews">
<div *ngFor="let view of savedViews" [formGroupName]="view.id" class="form-row">
<div class="form-group col-4 mr-3">
<label for="name_{{view.id}}">Name</label>
<input type="text" class="form-control" formControlName="name" id="name_{{view.id}}">
</div>
<div class="form-group col-auto mr-3">
<label for="show_on_dashboard_{{view.id}}">Appears on</label>
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="show_on_dashboard_{{view.id}}" formControlName="show_on_dashboard">
<label class="custom-control-label" for="show_on_dashboard_{{view.id}}">Show on dashboard</label>
</div>
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="show_in_sidebar_{{view.id}}" formControlName="show_in_sidebar">
<label class="custom-control-label" for="show_in_sidebar_{{view.id}}">Show in sidebar</label>
</div>
</div>
<div class="form-group col-auto">
<label for="name_{{view.id}}">Actions</label>
<button type="button" class="btn btn-sm btn-outline-danger form-control" (click)="deleteSavedView(view)">Delete</button>
</div>
</div>
<div *ngIf="savedViews.length == 0">No saved views defined.</div>
</div>
</ng-template>
</li>
@@ -1,9 +1,10 @@
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { SavedViewConfig } from 'src/app/data/saved-view-config';
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
import { GENERAL_SETTINGS } from 'src/app/data/storage-keys';
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
import { 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';
@Component({
selector: 'app-settings',
@@ -12,24 +13,63 @@ import { SavedViewConfigService } from 'src/app/services/saved-view-config.servi
})
export class SettingsComponent implements OnInit {
savedViewGroup = new FormGroup({})
settingsForm = new FormGroup({
'documentListItemPerPage': new FormControl(+localStorage.getItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE) || GENERAL_SETTINGS.DOCUMENT_LIST_SIZE_DEFAULT)
'documentListItemPerPage': new FormControl(+localStorage.getItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE) || GENERAL_SETTINGS.DOCUMENT_LIST_SIZE_DEFAULT),
'savedViews': this.savedViewGroup
})
constructor(
private savedViewConfigService: SavedViewConfigService,
private documentListViewService: DocumentListViewService
public savedViewService: SavedViewService,
private documentListViewService: DocumentListViewService,
private toastService: ToastService
) { }
ngOnInit(): void {
savedViews: PaperlessSavedView[]
ngOnInit() {
this.savedViewService.listAll().subscribe(r => {
this.savedViews = r.results
for (let view of this.savedViews) {
this.savedViewGroup.addControl(view.id.toString(), new FormGroup({
"id": new FormControl(view.id),
"name": new FormControl(view.name),
"show_on_dashboard": new FormControl(view.show_on_dashboard),
"show_in_sidebar": new FormControl(view.show_in_sidebar)
}))
}
})
}
deleteViewConfig(config: SavedViewConfig) {
this.savedViewConfigService.deleteConfig(config)
deleteSavedView(savedView: PaperlessSavedView) {
this.savedViewService.delete(savedView).subscribe(() => {
this.savedViewGroup.removeControl(savedView.id.toString())
this.savedViews.splice(this.savedViews.indexOf(savedView), 1)
this.toastService.showToast(Toast.make("Information", `Saved view "${savedView.name} deleted.`))
})
}
private saveLocalSettings() {
localStorage.setItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE, this.settingsForm.value.documentListItemPerPage)
this.documentListViewService.updatePageSize()
this.toastService.showToast(Toast.make("Information", "Settings saved successfully."))
}
saveSettings() {
localStorage.setItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE, this.settingsForm.value.documentListItemPerPage)
this.documentListViewService.updatePageSize()
let x = []
for (let id in this.savedViewGroup.value) {
x.push(this.savedViewGroup.value[id])
}
if (x.length > 0) {
this.savedViewService.patchMany(x).subscribe(s => {
this.saveLocalSettings()
}, error => {
this.toastService.showToast(Toast.makeError(`Error while storing settings on server: ${JSON.stringify(error.error)}`))
})
} else {
this.saveLocalSettings()
}
}
}
@@ -7,11 +7,21 @@
</div>
<div class="modal-body">
<app-input-text title="Name" formControlName="name"></app-input-text>
<app-input-select title="Colour" [items]="getColours()" formControlName="colour" [textColor]="getColor(objectForm.value.colour).textColor" [backgroundColor]="getColor(objectForm.value.colour).value"></app-input-select>
<div class="form-group paperless-input-select">
<label for="colour">Colour</label>
<ng-select name="colour" formControlName="colour" [items]="getColours()" bindValue="id" bindLabel="name" [clearable]="false">
<ng-template ng-option-tmp ng-label-tmp let-item="item">
<span class="badge" [style.background]="item.value" [style.color]="item.textColor">{{item.name}}</span>
</ng-template>
</ng-select>
</div>
<app-input-check title="Inbox tag" formControlName="is_inbox_tag" hint="Inbox tags are automatically assigned to all consumed documents."></app-input-check>
<app-input-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 title="Match" formControlName="match" hint="Auto matching does not require you to fill in this field."></app-input-text>
<app-input-check title="Case insensitive" formControlName="is_insensitive" hint="Auto matching ignores this option."></app-input-check>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button>
@@ -9,7 +9,7 @@
aria-label="Default pagination"></ngb-pagination>
</div>
<table class="table table-striped border shadow">
<table class="table table-striped border shadow-sm">
<thead>
<tr>
<th scope="col" sortable="name" (sort)="onSort($event)">Name</th>
@@ -28,8 +28,25 @@
<td scope="row">{{ tag.document_count }}</td>
<td scope="row">
<div class="btn-group">
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(tag)">Edit</button>
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(tag)">Delete</button>
<button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(tag)">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-funnel" viewBox="0 0 16 16">
<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>
Documents
</button>
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(tag)">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<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
</button>
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(tag)">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
<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>
Delete
</button>
</div>
</td>
</tr>
Loaded 100 of 192 files, more files were not shown because too many files have changed in this diff. Show more