mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-01-08 03:11:21 +00:00
Compare commits
259 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b9ea5c60f | ||
|
|
0fa3c3188d | ||
|
|
ebcb2cf694 | ||
|
|
8dc15352ef | ||
|
|
403de42031 | ||
|
|
b08a5e62d8 | ||
|
|
978bb3c339 | ||
|
|
3e389a3140 | ||
|
|
0dbd598116 | ||
|
|
01d10da061 | ||
|
|
0aad31b4bc | ||
|
|
2f599ca1d3 | ||
|
|
cd8f99d2c3 | ||
|
|
6968e228e1 | ||
|
|
b7d310ef90 | ||
|
|
1c3b85249c | ||
|
|
147a8774c6 | ||
|
|
9ace199422 | ||
|
|
f42e5bf1e5 | ||
|
|
5863060585 | ||
|
|
e466158ce2 | ||
|
|
38156123d4 | ||
|
|
ae3e2a7063 | ||
|
|
c527b274b6 | ||
|
|
5892941d8a | ||
|
|
b653e44f65 | ||
|
|
e75534c0f2 | ||
|
|
665863e395 | ||
|
|
c6b9e2b544 | ||
|
|
240d5b9da2 | ||
|
|
b10e7abbe8 | ||
|
|
01c2fe508e | ||
|
|
ee31fdc650 | ||
|
|
7f9a0204b5 | ||
|
|
32224f187d | ||
|
|
3f94fc2618 | ||
|
|
e79c45c98d | ||
|
|
bb814da95b | ||
|
|
fad3df1e39 | ||
|
|
1b1b57eb6a | ||
|
|
57a5a4147b | ||
|
|
37237dfcf6 | ||
|
|
3cdd38cb70 | ||
|
|
f184e6b162 | ||
|
|
5517c049b8 | ||
|
|
86f83f2bc4 | ||
|
|
557abbc17e | ||
|
|
e0293db16d | ||
|
|
2d841e7167 | ||
|
|
f214fe1b3e | ||
|
|
e5fe515b69 | ||
|
|
dfb88ebf83 | ||
|
|
789abb3bbb | ||
|
|
273c474e3f | ||
|
|
b262ec4b32 | ||
|
|
c05de3d57f | ||
|
|
55c4c690ef | ||
|
|
e10a2391c4 | ||
|
|
9b244d0265 | ||
|
|
cfc1ca45fc | ||
|
|
1c4e3f682e | ||
|
|
75b22e8684 | ||
|
|
ca2cb694d0 | ||
|
|
cea34211b6 | ||
|
|
d252d040bf | ||
|
|
43c88ae0f0 | ||
|
|
93be4e98d5 | ||
|
|
659cd3e9d5 | ||
|
|
2c3eaadbce | ||
|
|
1947d67e69 | ||
|
|
35dcc54dc8 | ||
|
|
48796e6961 | ||
|
|
164418880a | ||
|
|
fbca412d30 | ||
|
|
70347bb8f3 | ||
|
|
eaf11ea134 | ||
|
|
a59f8cd1b1 | ||
|
|
e9affbc1cf | ||
|
|
aa8789ae31 | ||
|
|
5c310c51d4 | ||
|
|
cf3fa50b55 | ||
|
|
7f933d373f | ||
|
|
ece94379d8 | ||
|
|
aa714bded3 | ||
|
|
ecfae9dadd | ||
|
|
69c04a209a | ||
|
|
e4ec52ed29 | ||
|
|
50aedc1094 | ||
|
|
062f8e5a73 | ||
|
|
c813c02025 | ||
|
|
28f45d8f15 | ||
|
|
8e339789fa | ||
|
|
dec17a3b9b | ||
|
|
6b60501dc7 | ||
|
|
0e78f32009 | ||
|
|
b13ec571f8 | ||
|
|
9f5e6d1969 | ||
|
|
b2e0a8c884 | ||
|
|
e47b105185 | ||
|
|
8bd82f5c69 | ||
|
|
e528a587cc | ||
|
|
1a526ac31e | ||
|
|
22e56f09ba | ||
|
|
af0eafbb3b | ||
|
|
8e57d4e791 | ||
|
|
b2a9cf4709 | ||
|
|
677cfb7a1e | ||
|
|
beffde1051 | ||
|
|
71c58c4b05 | ||
|
|
02e67d25b4 | ||
|
|
d208ab1e12 | ||
|
|
55075619c1 | ||
|
|
7dce57b9f7 | ||
|
|
a0c74025e3 | ||
|
|
31bea6a361 | ||
|
|
5894060dc5 | ||
|
|
b787983e42 | ||
|
|
7e0aa7136a | ||
|
|
56204933b0 | ||
|
|
49be87fe37 | ||
|
|
30185d560c | ||
|
|
6e614eda5e | ||
|
|
164755c755 | ||
|
|
6d39dfeb3b | ||
|
|
999b36473c | ||
|
|
8b57967836 | ||
|
|
ff71b04848 | ||
|
|
67d03c11b9 | ||
|
|
533be7e96e | ||
|
|
4ed56e4603 | ||
|
|
45848f5e34 | ||
|
|
cf619d9d31 | ||
|
|
de87efc291 | ||
|
|
889fe5890d | ||
|
|
958acd8a36 | ||
|
|
381a503947 | ||
|
|
b7126030d1 | ||
|
|
13d934dc6e | ||
|
|
926e746005 | ||
|
|
94c07839a4 | ||
|
|
277e668e07 | ||
|
|
10440ec820 | ||
|
|
02c1d496d6 | ||
|
|
98ab79ad5a | ||
|
|
e1fef59e82 | ||
|
|
32201dd034 | ||
|
|
670b6d3629 | ||
|
|
54d90a4c4b | ||
|
|
9bfc92cf79 | ||
|
|
a12ec00827 | ||
|
|
8cc0336338 | ||
|
|
bad7caa8b9 | ||
|
|
251fc582e9 | ||
|
|
245af65841 | ||
|
|
ee7492cf52 | ||
|
|
8e5c2a2b14 | ||
|
|
1343e4c99f | ||
|
|
89cb1211e7 | ||
|
|
04bb7d4893 | ||
|
|
1fafb9ace6 | ||
|
|
ae51619243 | ||
|
|
7ac101d84e | ||
|
|
2c18f6268b | ||
|
|
7d212f6e80 | ||
|
|
3d8cd0f0d6 | ||
|
|
3f719a21e0 | ||
|
|
7a75d3f41b | ||
|
|
1ddad84985 | ||
|
|
bcdbc975d6 | ||
|
|
a61ea3555a | ||
|
|
2de546fd52 | ||
|
|
d6894d3c64 | ||
|
|
bb1725c7dd | ||
|
|
6f684f8070 | ||
|
|
7712230300 | ||
|
|
7e4c5af158 | ||
|
|
2dc3019083 | ||
|
|
b5a85caa72 | ||
|
|
bb9b438aa6 | ||
|
|
3089b049cf | ||
|
|
5bea5e75c0 | ||
|
|
7906d8fef1 | ||
|
|
30853e963e | ||
|
|
1379c039b8 | ||
|
|
37c21e518d | ||
|
|
e215e11417 | ||
|
|
2374506a20 | ||
|
|
1c4d19198f | ||
|
|
e77be6a2ed | ||
|
|
1fdf1ef337 | ||
|
|
f5cc5fbaa3 | ||
|
|
a0631413d6 | ||
|
|
8ce4434ba9 | ||
|
|
f6a50ee7c6 | ||
|
|
beff45a835 | ||
|
|
dfa1f29809 | ||
|
|
2a57f9d6e5 | ||
|
|
02871e1e22 | ||
|
|
9cd40e96f4 | ||
|
|
bf9051e44d | ||
|
|
ebb39b13f0 | ||
|
|
4f14e0f425 | ||
|
|
e7cb358536 | ||
|
|
a85792e327 | ||
|
|
f5df910894 | ||
|
|
80b47fa287 | ||
|
|
1aa76f8483 | ||
|
|
de5d360d52 | ||
|
|
f0ad92e542 | ||
|
|
0b7ffa31d1 | ||
|
|
b452816a29 | ||
|
|
d1d09ac6ac | ||
|
|
66240188c7 | ||
|
|
d1f285113d | ||
|
|
56dfc71bb9 | ||
|
|
a8f27f79dd | ||
|
|
2c702eb568 | ||
|
|
63a58ccc38 | ||
|
|
4b0027797a | ||
|
|
66d6d29c23 | ||
|
|
fbb3a069cd | ||
|
|
a37796d0cf | ||
|
|
0b4c860354 | ||
|
|
a4a08aa667 | ||
|
|
c24bfd4d2b | ||
|
|
ed480f62e3 | ||
|
|
66aa7319ab | ||
|
|
4146955f4a | ||
|
|
57504b7ee6 | ||
|
|
364df5c050 | ||
|
|
f83185bfe4 | ||
|
|
db02d68a5a | ||
|
|
9a18954686 | ||
|
|
edcb62476b | ||
|
|
3a82b7806a | ||
|
|
51b1528fee | ||
|
|
defa80d05a | ||
|
|
abd54eeb3a | ||
|
|
2f7bb01f34 | ||
|
|
25e1177198 | ||
|
|
ab8a1cfded | ||
|
|
fa5121082d | ||
|
|
f0d86130ec | ||
|
|
ed236460b5 | ||
|
|
a4f7c5ddcb | ||
|
|
4fbb814e5b | ||
|
|
0f635d1bb2 | ||
|
|
0d48aea308 | ||
|
|
06a3fff2bc | ||
|
|
c28f19c9cf | ||
|
|
da87542a52 | ||
|
|
23ba3be68f | ||
|
|
f3fd0fcf72 | ||
|
|
72706a335d | ||
|
|
5369e0be03 | ||
|
|
8699b6931c | ||
|
|
35124023f0 | ||
|
|
fd4c9a1758 | ||
|
|
32186e0de1 |
37
README.md
37
README.md
@@ -7,7 +7,7 @@
|
||||
|
||||
[Paperless](https://github.com/the-paperless-project/paperless) is an application by Daniel Quinn and others that indexes your scanned documents and allows you to easily search for documents and store metadata alongside your documents.
|
||||
|
||||
Paperless-ng is a fork of the original project, adding a new interface and many other changes under the hood. For a detailed list of changes, see below.
|
||||
Paperless-ng is a fork of the original project, adding a new interface and many other changes under the hood. For a detailed list of changes, have a look at the changelog in the documentation.
|
||||
|
||||
This project is still in development and some things may not work as expected.
|
||||
|
||||
@@ -15,11 +15,13 @@ This project is still in development and some things may not work as expected.
|
||||
|
||||
Paperless does not control your scanner, it only helps you deal with what your scanner produces.
|
||||
|
||||
1. Buy a document scanner that can write to a place on your network. If you need some inspiration, have a look at the [scanner recommendations](https://paperless-ng.readthedocs.io/en/latest/scanners.html) page.
|
||||
2. Set it up to "scan to FTP" or something similar. It should be able to push scanned images to a server without you having to do anything. Of course if your scanner doesn't know how to automatically upload the file somewhere, you can always do that manually. Paperless doesn't care how the documents get into its local consumption directory.
|
||||
3. Have the target server run the Paperless consumption script to OCR the file and index it into a local database.
|
||||
4. Use the web frontend to sift through the database and find what you want.
|
||||
5. Download the PDF you need/want via the web interface and do whatever you like with it. You can even print it and send it as if it's the original. In most cases, no one will care or notice.
|
||||
1. Buy a document scanner that can write to a place on your network. If you need some inspiration, have a look at the [scanner recommendations](https://paperless-ng.readthedocs.io/en/latest/scanners.html) page. Set it up to "scan to FTP" or something similar. It should be able to push scanned images to a server without you having to do anything. Of course if your scanner doesn't know how to automatically upload the file somewhere, you can always do that manually. Paperless doesn't care how the documents get into its local consumption directory.
|
||||
|
||||
- Alternatively, you can use any of the mobile scanning apps out there. We have an app that allows you to share documents with paperless, if you're on Android. See the section on affiliated projects.
|
||||
|
||||
2. Wait for paperless to process your files. OCR is expensive, and depending on the power of your machine, this might take a bit of time.
|
||||
3. Use the web frontend to sift through the database and find what you want.
|
||||
4. Download the PDF you need/want via the web interface and do whatever you like with it. You can even print it and send it as if it's the original. In most cases, no one will care or notice.
|
||||
|
||||
Here's what you get:
|
||||
|
||||
@@ -28,17 +30,20 @@ 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.
|
||||
* Machine learning powered document matching.
|
||||
* Paperless learns from your documents and will be able to automatically assign tags, correspondents and types to documents once you've stored a few documents in paperless.
|
||||
* We have a mobile app that offers a 'Share with paperless' option over at https://github.com/qcasey/paperless_share. You can use that in combination with any of the mobile scanning apps out there. It's still a little rough around the edges, but it works!
|
||||
* A task processor that processes documents in parallel and also tells you when something goes wrong. On modern multi core systems, consumption is blazing fast.
|
||||
* Code cleanup in many, MANY areas. Some of the code from OG paperless was just overly complicated.
|
||||
* More tests, more stability.
|
||||
@@ -49,6 +54,8 @@ 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%.
|
||||
- Fix whatever bugs I and you find.
|
||||
@@ -57,7 +64,6 @@ For a complete list of changes from paperless, check out the [changelog](https:/
|
||||
|
||||
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.
|
||||
|
||||
@@ -15,7 +15,7 @@ services:
|
||||
POSTGRES_PASSWORD: paperless
|
||||
|
||||
webserver:
|
||||
image: jonaswinkler/paperless-ng:0.9.6
|
||||
image: jonaswinkler/paperless-ng:0.9.9
|
||||
restart: always
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
@@ -5,7 +5,7 @@ services:
|
||||
restart: always
|
||||
|
||||
webserver:
|
||||
image: jonaswinkler/paperless-ng:0.9.6
|
||||
image: jonaswinkler/paperless-ng:0.9.9
|
||||
restart: always
|
||||
depends_on:
|
||||
- broker
|
||||
|
||||
@@ -5,85 +5,6 @@ Advanced topics
|
||||
Paperless offers a couple features that automate certain tasks and make your life
|
||||
easier.
|
||||
|
||||
Guesswork
|
||||
#########
|
||||
|
||||
|
||||
Any document you put into the consumption directory will be consumed, but if
|
||||
you name the file right, it'll automatically set some values in the database
|
||||
for you. This is is the logic the consumer follows:
|
||||
|
||||
1. Try to find the correspondent, title, and tags in the file name following
|
||||
the pattern: ``Date - Correspondent - Title - tag,tag,tag.pdf``. Note that
|
||||
the format of the date is **rigidly defined** as ``YYYYMMDDHHMMSSZ`` or
|
||||
``YYYYMMDDZ``. The ``Z`` refers "Zulu time" AKA "UTC".
|
||||
The tags are optional, so the format ``Date - Correspondent - Title.pdf``
|
||||
works as well.
|
||||
2. If that doesn't work, we skip the date and try this pattern:
|
||||
``Correspondent - Title - tag,tag,tag.pdf``.
|
||||
3. If that doesn't work, we try to find the correspondent and title in the file
|
||||
name following the pattern: ``Correspondent - Title.pdf``.
|
||||
4. If that doesn't work, just assume that the name of the file is the title.
|
||||
|
||||
So given the above, the following examples would work as you'd expect:
|
||||
|
||||
* ``20150314000700Z - Some Company Name - Invoice 2016-01-01 - money,invoices.pdf``
|
||||
* ``20150314Z - Some Company Name - Invoice 2016-01-01 - money,invoices.pdf``
|
||||
* ``Some Company Name - Invoice 2016-01-01 - money,invoices.pdf``
|
||||
* ``Another Company - Letter of Reference.jpg``
|
||||
* ``Dad's Recipe for Pancakes.png``
|
||||
|
||||
These however wouldn't work:
|
||||
|
||||
* ``2015-03-14 00:07:00 UTC - Some Company Name, Invoice 2016-01-01, money, invoices.pdf``
|
||||
* ``2015-03-14 - Some Company Name, Invoice 2016-01-01, money, invoices.pdf``
|
||||
* ``Some Company Name, Invoice 2016-01-01, money, invoices.pdf``
|
||||
* ``Another Company- Letter of Reference.jpg``
|
||||
|
||||
Do I have to be so strict about naming?
|
||||
=======================================
|
||||
|
||||
Rather than using the strict document naming rules, one can also set the option
|
||||
``PAPERLESS_FILENAME_DATE_ORDER`` in ``paperless.conf`` to any date order
|
||||
that is accepted by dateparser_. Doing so will cause ``paperless`` to default
|
||||
to any date format that is found in the title, instead of a date pulled from
|
||||
the document's text, without requiring the strict formatting of the document
|
||||
filename as described above.
|
||||
|
||||
.. _dateparser: https://github.com/scrapinghub/dateparser/blob/v0.7.0/docs/usage.rst#settings
|
||||
|
||||
.. _advanced-transforming_filenames:
|
||||
|
||||
Transforming filenames for parsing
|
||||
==================================
|
||||
|
||||
Some devices can't produce filenames that can be parsed by the default
|
||||
parser. By configuring the option ``PAPERLESS_FILENAME_PARSE_TRANSFORMS`` in
|
||||
``paperless.conf`` one can add transformations that are applied to the filename
|
||||
before it's parsed.
|
||||
|
||||
The option contains a list of dictionaries of regular expressions (key:
|
||||
``pattern``) and replacements (key: ``repl``) in JSON format, which are
|
||||
applied in order by passing them to ``re.subn``. Transformation stops
|
||||
after the first match, so at most one transformation is applied. The general
|
||||
syntax is
|
||||
|
||||
.. code:: python
|
||||
|
||||
[{"pattern":"pattern1", "repl":"repl1"}, {"pattern":"pattern2", "repl":"repl2"}, ..., {"pattern":"patternN", "repl":"replN"}]
|
||||
|
||||
The example below is for a Brother ADS-2400N, a scanner that allows
|
||||
different names to different hardware buttons (useful for handling
|
||||
multiple entities in one instance), but insists on adding ``_<count>``
|
||||
to the filename.
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Brother profile configuration, support "Name_Date_Count" (the default
|
||||
# setting) and "Name_Count" (use "Name" as tag and "Count" as title).
|
||||
PAPERLESS_FILENAME_PARSE_TRANSFORMS=[{"pattern":"^([a-z]+)_(\\d{8})_(\\d{6})_([0-9]+)\\.", "repl":"\\2\\3Z - \\4 - \\1."}, {"pattern":"^([a-z]+)_([0-9]+)\\.", "repl":" - \\2 - \\1."}]
|
||||
|
||||
|
||||
.. _advanced-matching:
|
||||
|
||||
Matching tags, correspondents and document types
|
||||
@@ -263,10 +184,10 @@ using the identifier which it has assigned to each document. You will end up get
|
||||
files like ``0000123.pdf`` in your media directory. This isn't necessarily a bad
|
||||
thing, because you normally don't have to access these files manually. However, if
|
||||
you wish to name your files differently, you can do that by adjusting the
|
||||
``PAPERLESS_FILENAME_FORMAT`` settings variable.
|
||||
``PAPERLESS_FILENAME_FORMAT`` configuration option.
|
||||
|
||||
This variable allows you to configure the filename (folders are allowed!) using
|
||||
placeholders. For example, setting
|
||||
This variable allows you to configure the filename (folders are allowed) using
|
||||
placeholders. For example, configuring this to
|
||||
|
||||
.. code:: bash
|
||||
|
||||
@@ -277,17 +198,16 @@ will create a directory structure as follows:
|
||||
.. code::
|
||||
|
||||
2019/
|
||||
my_bank/
|
||||
statement-january-0000001.pdf
|
||||
statement-february-0000002.pdf
|
||||
My bank/
|
||||
Statement January.pdf
|
||||
Statement February.pdf
|
||||
2020/
|
||||
my_bank/
|
||||
statement-january-0000003.pdf
|
||||
shoe_store/
|
||||
my_new_shoes-0000004.pdf
|
||||
|
||||
Paperless appends the unique identifier of each document to the filename. This
|
||||
avoids filename clashes.
|
||||
My bank/
|
||||
Statement January.pdf
|
||||
Letter.pdf
|
||||
Letter_01.pdf
|
||||
Shoe store/
|
||||
My new shoes.pdf
|
||||
|
||||
.. danger::
|
||||
|
||||
@@ -299,6 +219,7 @@ Paperless provides the following placeholders withing filenames:
|
||||
|
||||
* ``{correspondent}``: The name of the correspondent, or "none".
|
||||
* ``{document_type}``: The name of the document type, or "none".
|
||||
* ``{tag_list}``: A comma separated list of all tags assigned to the document.
|
||||
* ``{title}``: The title of the document.
|
||||
* ``{created}``: The full date and time the document was created.
|
||||
* ``{created_year}``: Year created only.
|
||||
@@ -309,8 +230,14 @@ Paperless provides the following placeholders withing filenames:
|
||||
* ``{added_month}``: Month added only (number 1-12).
|
||||
* ``{added_day}``: Day added only (number 1-31).
|
||||
|
||||
Paperless will convert all values for the placeholders into values which are safe
|
||||
for use in filenames.
|
||||
|
||||
Paperless will try to conserve the information from your database as much as possible.
|
||||
However, some characters that you can use in document titles and correspondent names (such
|
||||
as ``: \ /`` and a couple more) are not allowed in filenames and will be replaced with dashes.
|
||||
|
||||
If paperless detects that two documents share the same filename, paperless will automatically
|
||||
append ``_01``, ``_02``, etc to the filename. This happens if all the placeholders in a filename
|
||||
evaluate to the same value.
|
||||
|
||||
.. hint::
|
||||
|
||||
|
||||
15
docs/api.rst
15
docs/api.rst
@@ -221,21 +221,16 @@ Each fragment contains a list of strings, and some of them are marked as a highl
|
||||
|
||||
[
|
||||
[
|
||||
{"text": "This is a sample text with a "},
|
||||
{"text": "highlighted", "term": 0},
|
||||
{"text": " word."}
|
||||
{"text": "This is a sample text with a ", "highlight": false},
|
||||
{"text": "highlighted", "highlight": true},
|
||||
{"text": " word.", "highlight": false}
|
||||
],
|
||||
[
|
||||
{"text": "Another", "term": 1},
|
||||
{"text": " fragment with a highlight."}
|
||||
{"text": "Another", "highlight": true},
|
||||
{"text": " fragment with a highlight.", "highlight": false}
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
|
||||
When ``term`` is present within a string, the word within ``text`` should be highlighted.
|
||||
The term index groups multiple matches together and words with the same index
|
||||
should get identical highlighting.
|
||||
A client may use this example to produce the following output:
|
||||
|
||||
... This is a sample text with a **highlighted** word. ... **Another** fragment with a highlight. ...
|
||||
|
||||
@@ -5,6 +5,92 @@
|
||||
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
|
||||
##################
|
||||
|
||||
@@ -841,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
|
||||
|
||||
@@ -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
|
||||
########
|
||||
|
||||
@@ -78,6 +78,12 @@ that automatically, I'm all ears. For now, you have to grab the latest release
|
||||
archive from the project page and build the image yourself. The release comes
|
||||
with the front end already compiled, so you don't have to do this on the Pi.
|
||||
|
||||
**Q:** *How do I run this on unRaid?*
|
||||
|
||||
**A:** Head over to `<https://github.com/selfhosters/unRAID-CA-templates>`_,
|
||||
`Uli Fahrer <https://github.com/Tooa>`_ created a container template for that.
|
||||
I don't exactly know how to use that though, since I don't use unRaid.
|
||||
|
||||
**Q:** *How do I run this on my toaster?*
|
||||
|
||||
**A:** I honestly don't know! As for all other devices that might be able
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -57,9 +57,6 @@ Adding documents to paperless
|
||||
#############################
|
||||
|
||||
Once you've got Paperless setup, you need to start feeding documents into it.
|
||||
Currently, there are four options: the consumption directory, the dashboard, IMAP (email), and
|
||||
HTTP POST.
|
||||
|
||||
When adding documents to paperless, it will perform the following operations on
|
||||
your documents:
|
||||
|
||||
@@ -112,6 +109,17 @@ Dashboard upload
|
||||
The dashboard has a file drop field to upload documents to paperless. Simply drag a file
|
||||
onto this field or select a file with the file dialog. Multiple files are supported.
|
||||
|
||||
|
||||
Mobile upload
|
||||
=============
|
||||
|
||||
The mobile app over at `<https://github.com/qcasey/paperless_share>`_ allows Android users
|
||||
to share any documents with paperless. This can be combined with any of the mobile
|
||||
scanning apps out there, such as Office Lens.
|
||||
|
||||
Furthermore, there is the `Paperless App <https://github.com/bauerj/paperless_app>`_ as well,
|
||||
which no only has document upload, but also document editing and browsing.
|
||||
|
||||
.. _usage-email:
|
||||
|
||||
IMAP (Email)
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#PAPERLESS_FORCE_SCRIPT_NAME=
|
||||
#PAPERLESS_STATIC_URL=/static/
|
||||
#PAPERLESS_AUTO_LOGIN_USERNAME=
|
||||
#PAPERLESS_COOKIE_PREFIX=
|
||||
|
||||
# OCR settings
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
71
src-ui/package-lock.json
generated
71
src-ui/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,9 +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: [
|
||||
@@ -60,10 +68,9 @@ import { FileSizePipe } from './pipes/file-size.pipe';
|
||||
DocumentTypeListComponent,
|
||||
LogsComponent,
|
||||
SettingsComponent,
|
||||
SafePipe,
|
||||
NotFoundComponent,
|
||||
CorrespondentEditDialogComponent,
|
||||
DeleteDialogComponent,
|
||||
ConfirmDialogComponent,
|
||||
TagEditDialogComponent,
|
||||
DocumentTypeEditDialogComponent,
|
||||
TagComponent,
|
||||
@@ -73,6 +80,9 @@ import { FileSizePipe } from './pipes/file-size.pipe';
|
||||
AppFrameComponent,
|
||||
ToastsComponent,
|
||||
FilterEditorComponent,
|
||||
FilterDropdownComponent,
|
||||
FilterDropdownButtonComponent,
|
||||
FilterDropdownDateComponent,
|
||||
DocumentCardLargeComponent,
|
||||
DocumentCardSmallComponent,
|
||||
TextComponent,
|
||||
@@ -88,7 +98,11 @@ import { FileSizePipe } from './pipes/file-size.pipe';
|
||||
WidgetFrameComponent,
|
||||
WelcomeWidgetComponent,
|
||||
YesNoPipe,
|
||||
FileSizePipe
|
||||
FileSizePipe,
|
||||
FilterPipe,
|
||||
DocumentTitlePipe,
|
||||
MetadataCollapseComponent,
|
||||
SelectDialogComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@@ -98,7 +112,9 @@ import { FileSizePipe } from './pipes/file-size.pipe';
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgxFileDropModule,
|
||||
InfiniteScrollModule
|
||||
InfiniteScrollModule,
|
||||
PdfViewerModule,
|
||||
NgSelectModule
|
||||
],
|
||||
providers: [
|
||||
DatePipe,
|
||||
@@ -106,7 +122,9 @@ import { FileSizePipe } from './pipes/file-size.pipe';
|
||||
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()
|
||||
|
||||
@@ -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: [{
|
||||
|
||||
@@ -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 shadow">
|
||||
<button type="button" *ngFor="let tag of tags" ngbDropdownItem (click)="addTag(tag.id)">
|
||||
<app-tag [tag]="tag"></app-tag>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template ng-label-tmp let-item="item">
|
||||
<span class="tag-wrap tag-wrap-delete" (click)="removeTag(item.id)">
|
||||
<svg width="1.2em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg>
|
||||
<app-tag style="background-color: none;" [tag]="getTag(item.id)"></app-tag>
|
||||
</span>
|
||||
</ng-template>
|
||||
<ng-template ng-option-tmp let-item="item" let-index="index" let-search="searchTerm">
|
||||
<div class="tag-wrap">
|
||||
<div class="selected-icon d-inline-block mr-1">
|
||||
<svg *ngIf="displayValue.includes(item.id)" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#check"/>
|
||||
</svg>
|
||||
</div>
|
||||
<app-tag class="mr-2" [tag]="getTag(item.id)"></app-tag>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-select>
|
||||
|
||||
<div class="input-group-append">
|
||||
|
||||
<button class="btn btn-outline-secondary" type="button" (click)="createTag()">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<small class="form-text text-muted" *ngIf="hint">{{hint}}</small>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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,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">×</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,7 +1,7 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { SavedViewConfigService } from 'src/app/services/saved-view-config.service';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { Meta } from '@angular/platform-browser';
|
||||
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
|
||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
|
||||
|
||||
|
||||
@Component({
|
||||
@@ -12,15 +12,36 @@ import { environment } from 'src/environments/environment';
|
||||
export class DashboardComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
public savedViewConfigService: SavedViewConfigService,
|
||||
private titleService: Title) { }
|
||||
private savedViewService: SavedViewService,
|
||||
private meta: Meta
|
||||
) { }
|
||||
|
||||
get displayName() {
|
||||
let tagFullName = this.meta.getTag('name=full_name')
|
||||
let tagUsername = this.meta.getTag('name=username')
|
||||
if (tagFullName && tagFullName.content) {
|
||||
return tagFullName.content
|
||||
} else if (tagUsername && tagUsername.content) {
|
||||
return tagUsername.content
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
savedViews = []
|
||||
get subtitle() {
|
||||
if (this.displayName) {
|
||||
return `Hello ${this.displayName}, welcome to Paperless-ng!`
|
||||
} else {
|
||||
return `Welcome to Paperless-ng!`
|
||||
}
|
||||
}
|
||||
|
||||
savedViews: PaperlessSavedView[] = []
|
||||
|
||||
ngOnInit(): void {
|
||||
this.savedViews = this.savedViewConfigService.getDashboardConfigs()
|
||||
this.titleService.setTitle(`Dashboard - ${environment.appTitle}`)
|
||||
this.savedViewService.listAll().subscribe(results => {
|
||||
this.savedViews = results.results.filter(savedView => savedView.show_on_dashboard)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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,18 +18,18 @@ export class SavedViewWidgetComponent implements OnInit {
|
||||
private list: DocumentListViewService) { }
|
||||
|
||||
@Input()
|
||||
savedView: SavedViewConfig
|
||||
savedView: PaperlessSavedView
|
||||
|
||||
documents: PaperlessDocument[] = []
|
||||
|
||||
ngOnInit(): void {
|
||||
this.documentService.list(1,10,this.savedView.sortField,this.savedView.sortDirection,this.savedView.filterRules).subscribe(result => {
|
||||
this.documentService.listFiltered(1,10,this.savedView.sort_field, this.savedView.sort_reverse, this.savedView.filter_rules).subscribe(result => {
|
||||
this.documents = result.results
|
||||
})
|
||||
}
|
||||
|
||||
showAll() {
|
||||
if (this.savedView.showInSideBar) {
|
||||
if (this.savedView.show_in_sidebar) {
|
||||
this.router.navigate(['view', this.savedView.id])
|
||||
} else {
|
||||
this.list.load(this.savedView)
|
||||
|
||||
@@ -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" />
|
||||
@@ -24,6 +34,12 @@
|
||||
|
||||
</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,7 +51,7 @@
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xl">
|
||||
<div class="col mb-4">
|
||||
|
||||
<form [formGroup]='documentForm' (ngSubmit)="save()">
|
||||
|
||||
@@ -51,10 +67,10 @@
|
||||
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-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>
|
||||
|
||||
</ng-template>
|
||||
@@ -110,53 +126,8 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h6 *ngIf="metadata?.original_metadata.length > 0">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm mr-2"
|
||||
(click)="expandOriginalMetadata = !expandOriginalMetadata" aria-controls="collapseExample">
|
||||
<svg class="buttonicon" fill="currentColor" *ngIf="!expandOriginalMetadata">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#caret-down" />
|
||||
</svg>
|
||||
<svg class="buttonicon" fill="currentColor" *ngIf="expandOriginalMetadata">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#caret-up" />
|
||||
</svg>
|
||||
</button>
|
||||
Original document metadata
|
||||
</h6>
|
||||
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="!expandOriginalMetadata">
|
||||
<table class="table table-borderless">
|
||||
<tbody>
|
||||
<tr *ngFor="let m of metadata?.original_metadata">
|
||||
<td>{{m.prefix}}:{{m.key}}</td>
|
||||
<td>{{m.value}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h6 *ngIf="metadata?.has_archive_version && metadata?.archive_metadata.length > 0">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm mr-2"
|
||||
(click)="expandArchivedMetadata = !expandArchivedMetadata" aria-controls="collapseExample">
|
||||
<svg class="buttonicon" fill="currentColor" *ngIf="!expandArchivedMetadata">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#caret-down" />
|
||||
</svg>
|
||||
<svg class="buttonicon" fill="currentColor" *ngIf="expandArchivedMetadata">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#caret-up" />
|
||||
</svg>
|
||||
</button>
|
||||
Archived document metadata
|
||||
</h6>
|
||||
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="!expandArchivedMetadata">
|
||||
<table class="table table-borderless">
|
||||
<tbody>
|
||||
<tr *ngFor="let m of metadata?.archive_metadata">
|
||||
<td>{{m.prefix}}:{{m.key}}</td>
|
||||
<td>{{m.value}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<app-metadata-collapse 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>
|
||||
@@ -171,11 +142,9 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col-xl d-none d-xl-block document-preview">
|
||||
<object [data]="previewUrl | safe" type="application/pdf" width="100%" height="100%">
|
||||
<p>Your browser does not support PDFs.
|
||||
<a href="previewUrl">Download the PDF</a>.</p>
|
||||
</object>
|
||||
|
||||
<div class="col-md-6 col-xl-8 mb-3">
|
||||
<div class="pdf-viewer-container" *ngIf="getContentType() == 'application/pdf'">
|
||||
<pdf-viewer [src]="previewUrl" [original-size]="false" [show-borders]="true" [show-all]="true" [(page)]="previewCurrentPage" (after-load-complete)="pdfPreviewLoaded($event)"></pdf-viewer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.document-preview {
|
||||
height: calc(100vh - 180px);
|
||||
.pdf-viewer-container {
|
||||
height: calc(100vh - 160px);
|
||||
top: 70px;
|
||||
position: sticky;
|
||||
}
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
|
||||
import { PaperlessDocument } from 'src/app/data/paperless-document';
|
||||
import { PaperlessDocumentMetadata } from 'src/app/data/paperless-document-metadata';
|
||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
|
||||
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe';
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service';
|
||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
|
||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service';
|
||||
import { DocumentService } from 'src/app/services/rest/document.service';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { DeleteDialogComponent } from '../common/delete-dialog/delete-dialog.component';
|
||||
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component';
|
||||
import { CorrespondentEditDialogComponent } from '../manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component';
|
||||
import { DocumentTypeEditDialogComponent } from '../manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component';
|
||||
import { PDFDocumentProxy } from 'ng2-pdf-viewer';
|
||||
|
||||
@Component({
|
||||
selector: 'app-document-detail',
|
||||
@@ -48,8 +48,11 @@ export class DocumentDetailComponent implements OnInit {
|
||||
tags: new FormControl([])
|
||||
})
|
||||
|
||||
previewCurrentPage: number = 1
|
||||
previewNumPages: number = 1
|
||||
|
||||
constructor(
|
||||
private documentsService: DocumentService,
|
||||
private documentsService: DocumentService,
|
||||
private route: ActivatedRoute,
|
||||
private correspondentService: CorrespondentService,
|
||||
private documentTypeService: DocumentTypeService,
|
||||
@@ -57,7 +60,11 @@ export class DocumentDetailComponent implements OnInit {
|
||||
private modalService: NgbModal,
|
||||
private openDocumentService: OpenDocumentsService,
|
||||
private documentListViewService: DocumentListViewService,
|
||||
private titleService: Title) { }
|
||||
private documentTitlePipe: DocumentTitlePipe) { }
|
||||
|
||||
getContentType() {
|
||||
return this.metadata?.has_archive_version ? 'application/pdf' : this.metadata?.original_mime_type
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.documentForm.valueChanges.subscribe(wow => {
|
||||
@@ -86,11 +93,10 @@ export class DocumentDetailComponent implements OnInit {
|
||||
|
||||
updateComponent(doc: PaperlessDocument) {
|
||||
this.document = doc
|
||||
this.titleService.setTitle(`${doc.title} - ${environment.appTitle}`)
|
||||
this.documentsService.getMetadata(doc.id).subscribe(result => {
|
||||
this.metadata = result
|
||||
})
|
||||
this.title = doc.title
|
||||
this.title = this.documentTitlePipe.transform(doc.title)
|
||||
this.documentForm.patchValue(doc)
|
||||
}
|
||||
|
||||
@@ -124,7 +130,7 @@ export class DocumentDetailComponent implements OnInit {
|
||||
}, error => {this.router.navigate(['404'])})
|
||||
}
|
||||
|
||||
save() {
|
||||
save() {
|
||||
this.documentsService.update(this.document).subscribe(result => {
|
||||
this.close()
|
||||
})
|
||||
@@ -151,19 +157,31 @@ export class DocumentDetailComponent implements OnInit {
|
||||
}
|
||||
|
||||
delete() {
|
||||
let modal = this.modalService.open(DeleteDialogComponent, {backdrop: 'static'})
|
||||
modal.componentInstance.message = `Do you really want to delete document '${this.document.title}'?`
|
||||
modal.componentInstance.message2 = `The files for this document will be deleted permanently. This operation cannot be undone.`
|
||||
modal.componentInstance.deleteClicked.subscribe(() => {
|
||||
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
||||
modal.componentInstance.title = "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,7 +1,15 @@
|
||||
<div class="col p-2 h-100" style="width: 16rem;">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="border-bottom">
|
||||
<img class="card-img doc-img" [src]="getThumbUrl()">
|
||||
<div class="col p-2 h-100 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>
|
||||
@@ -11,18 +19,18 @@
|
||||
</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">
|
||||
@@ -42,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 {
|
||||
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,7 +1,6 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { PaperlessDocument } from 'src/app/data/paperless-document';
|
||||
import { PaperlessTag } from 'src/app/data/paperless-tag';
|
||||
import { DocumentService } from 'src/app/services/rest/document.service';
|
||||
|
||||
@Component({
|
||||
@@ -13,6 +12,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
|
||||
|
||||
|
||||
@@ -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,7 +44,8 @@
|
||||
</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" class="shadow">
|
||||
@@ -30,63 +54,56 @@
|
||||
</div>
|
||||
</div>
|
||||
<label ngbButtonLabel class="btn-outline-primary btn-sm">
|
||||
<input ngbButton type="radio" class="btn btn-sm" value="asc">
|
||||
<input ngbButton type="radio" class="btn btn-sm" [value]="false">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#sort-alpha-down" />
|
||||
</svg>
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-outline-primary btn-sm">
|
||||
<input ngbButton type="radio" class="btn btn-sm" value="des">
|
||||
<input ngbButton type="radio" class="btn btn-sm" [value]="true">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#sort-alpha-up-alt" />
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="btn-group ml-2">
|
||||
|
||||
<button type="button" class="btn btn-sm" [ngClass]="isFiltered ? 'btn-primary' : 'btn-outline-primary'" (click)="showFilter=!showFilter">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#funnel" />
|
||||
</svg>
|
||||
Filter
|
||||
</button>
|
||||
|
||||
<div class="btn-group" ngbDropdown role="group">
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle-split" ngbDropdownToggle></button>
|
||||
<div class="dropdown-menu" ngbDropdownMenu class="shadow">
|
||||
<ng-container *ngIf="!list.savedViewId" >
|
||||
<button ngbDropdownItem *ngFor="let config of savedViewConfigService.getConfigs()" (click)="loadViewConfig(config)">{{config.title}}</button>
|
||||
<div class="dropdown-divider" *ngIf="savedViewConfigService.getConfigs().length > 0"></div>
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle" ngbDropdownToggle>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) <span *ngIf="isFiltered">(filtered)</span></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-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" style="overflow-wrap: anywhere;">{{d.title}}</a>
|
||||
<app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ml-1" clickable="true" linkTitle="Filter by tag" (click)="filterByTag(t.id)"></app-tag>
|
||||
<a routerLink="/documents/{{d.id}}" title="Edit document" style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
|
||||
<app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ml-1" clickable="true" linkTitle="Filter by tag" (click)="clickTag(t.id)"></app-tag>
|
||||
</td>
|
||||
<td class="d-none d-xl-table-cell">
|
||||
<ng-container *ngIf="d.document_type">
|
||||
<a [routerLink]="" (click)="filterByDocumentType(d.document_type)" title="Filter by document type">{{(d.document_type$ | async)?.name}}</a>
|
||||
<a [routerLink]="" (click)="clickDocumentType(d.document_type)" title="Filter by document type">{{(d.document_type$ | async)?.name}}</a>
|
||||
</ng-container>
|
||||
</td>
|
||||
<td>
|
||||
@@ -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,16 +1,22 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { cloneFilterRules, FilterRule } from 'src/app/data/filter-rule';
|
||||
import { FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type';
|
||||
import { SavedViewConfig } from 'src/app/data/saved-view-config';
|
||||
import { 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 { environment } from 'src/environments/environment';
|
||||
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',
|
||||
@@ -21,17 +27,22 @@ export class DocumentListComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
public list: DocumentListViewService,
|
||||
public savedViewConfigService: SavedViewConfigService,
|
||||
public savedViewService: SavedViewService,
|
||||
public route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private toastService: ToastService,
|
||||
public modalService: NgbModal,
|
||||
private titleService: Title) { }
|
||||
private 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
|
||||
}
|
||||
@@ -53,93 +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.titleService.setTitle(`${this.list.savedView.title} - ${environment.appTitle}`)
|
||||
this.savedViewService.getCached(+params.get('id')).subscribe(view => {
|
||||
if (!view) {
|
||||
this.router.navigate(["404"])
|
||||
return
|
||||
}
|
||||
|
||||
this.list.savedView = view
|
||||
this.list.reload()
|
||||
})
|
||||
} else {
|
||||
this.list.savedView = null
|
||||
this.filterRules = this.list.filterRules
|
||||
this.showFilter = this.filterRules.length > 0
|
||||
this.titleService.setTitle(`Documents - ${environment.appTitle}`)
|
||||
this.list.reload()
|
||||
}
|
||||
this.list.clear()
|
||||
this.list.reload()
|
||||
})
|
||||
}
|
||||
|
||||
applyFilterRules() {
|
||||
this.list.filterRules = this.filterRules
|
||||
}
|
||||
|
||||
clearFilterRules() {
|
||||
this.list.filterRules = this.filterRules
|
||||
this.showFilter = false
|
||||
}
|
||||
|
||||
loadViewConfig(config: SavedViewConfig) {
|
||||
this.filterRules = cloneFilterRules(config.filterRules)
|
||||
this.list.load(config)
|
||||
loadViewConfig(view: PaperlessSavedView) {
|
||||
this.list.load(view)
|
||||
this.list.reload()
|
||||
}
|
||||
|
||||
saveViewConfig() {
|
||||
this.savedViewConfigService.updateConfig(this.list.savedView)
|
||||
this.toastService.showToast(Toast.make("Information", `View "${this.list.savedView.title}" saved successfully.`))
|
||||
this.savedViewService.update(this.list.savedView).subscribe(result => {
|
||||
this.toastService.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: this.selectedRuleType.default})
|
||||
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,9 +1,10 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
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 { environment } from 'src/environments/environment';
|
||||
import { GenericListComponent } from '../generic-list/generic-list.component';
|
||||
import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog/correspondent-edit-dialog.component';
|
||||
|
||||
@@ -12,9 +13,12 @@ import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog/co
|
||||
templateUrl: './correspondent-list.component.html',
|
||||
styleUrls: ['./correspondent-list.component.scss']
|
||||
})
|
||||
export class CorrespondentListComponent extends GenericListComponent<PaperlessCorrespondent> implements OnInit {
|
||||
export class CorrespondentListComponent extends GenericListComponent<PaperlessCorrespondent> {
|
||||
|
||||
constructor(correspondentsService: CorrespondentService, modalService: NgbModal, private titleService: Title) {
|
||||
constructor(correspondentsService: CorrespondentService, modalService: NgbModal,
|
||||
private router: Router,
|
||||
private list: DocumentListViewService
|
||||
) {
|
||||
super(correspondentsService,modalService,CorrespondentEditDialogComponent)
|
||||
}
|
||||
|
||||
@@ -22,9 +26,10 @@ export class CorrespondentListComponent extends GenericListComponent<PaperlessCo
|
||||
return `correspondent '${object.name}'`
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit()
|
||||
this.titleService.setTitle(`Correspondents - ${environment.appTitle}`)
|
||||
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,9 +1,10 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
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 { environment } from 'src/environments/environment';
|
||||
import { GenericListComponent } from '../generic-list/generic-list.component';
|
||||
import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog/document-type-edit-dialog.component';
|
||||
|
||||
@@ -12,9 +13,12 @@ import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog/doc
|
||||
templateUrl: './document-type-list.component.html',
|
||||
styleUrls: ['./document-type-list.component.scss']
|
||||
})
|
||||
export class DocumentTypeListComponent extends GenericListComponent<PaperlessDocumentType> implements OnInit {
|
||||
export class DocumentTypeListComponent extends GenericListComponent<PaperlessDocumentType> {
|
||||
|
||||
constructor(service: DocumentTypeService, modalService: NgbModal, private titleService: Title) {
|
||||
constructor(service: DocumentTypeService, modalService: NgbModal,
|
||||
private router: Router,
|
||||
private list: DocumentListViewService
|
||||
) {
|
||||
super(service, modalService, DocumentTypeEditDialogComponent)
|
||||
}
|
||||
|
||||
@@ -22,8 +26,10 @@ export class DocumentTypeListComponent extends GenericListComponent<PaperlessDoc
|
||||
return `document type '${object.name}'`
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit()
|
||||
this.titleService.setTitle(`Document types - ${environment.appTitle}`)
|
||||
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,8 +1,6 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { LOG_LEVELS, LOG_LEVEL_INFO, PaperlessLog } from 'src/app/data/paperless-log';
|
||||
import { LogService } from 'src/app/services/rest/log.service';
|
||||
import { environment } from 'src/environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'app-logs',
|
||||
@@ -11,18 +9,17 @@ import { environment } from 'src/environments/environment';
|
||||
})
|
||||
export class LogsComponent implements OnInit {
|
||||
|
||||
constructor(private logService: LogService, private titleService: Title) { }
|
||||
constructor(private logService: LogService) { }
|
||||
|
||||
logs: PaperlessLog[] = []
|
||||
level: number = LOG_LEVEL_INFO
|
||||
|
||||
ngOnInit(): void {
|
||||
this.reload()
|
||||
this.titleService.setTitle(`Logs - ${environment.appTitle}`)
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -34,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 | yesno }}</td>
|
||||
<td>{{ config.showInSideBar | yesno }}</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,11 +1,10 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
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 { environment } from 'src/environments/environment';
|
||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
|
||||
import { Toast, ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings',
|
||||
@@ -14,26 +13,63 @@ import { environment } from 'src/environments/environment';
|
||||
})
|
||||
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,
|
||||
public savedViewService: SavedViewService,
|
||||
private documentListViewService: DocumentListViewService,
|
||||
private titleService: Title
|
||||
private toastService: ToastService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.titleService.setTitle(`Settings - ${environment.appTitle}`)
|
||||
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>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { FILTER_HAS_TAG } from 'src/app/data/filter-rule-type';
|
||||
import { TAG_COLOURS, PaperlessTag } from 'src/app/data/paperless-tag';
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
||||
import { TagService } from 'src/app/services/rest/tag.service';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { GenericListComponent } from '../generic-list/generic-list.component';
|
||||
import { TagEditDialogComponent } from './tag-edit-dialog/tag-edit-dialog.component';
|
||||
|
||||
@@ -12,18 +13,15 @@ import { TagEditDialogComponent } from './tag-edit-dialog/tag-edit-dialog.compon
|
||||
templateUrl: './tag-list.component.html',
|
||||
styleUrls: ['./tag-list.component.scss']
|
||||
})
|
||||
export class TagListComponent extends GenericListComponent<PaperlessTag> implements OnInit {
|
||||
export class TagListComponent extends GenericListComponent<PaperlessTag> {
|
||||
|
||||
constructor(tagService: TagService, modalService: NgbModal, private titleService: Title) {
|
||||
constructor(tagService: TagService, modalService: NgbModal,
|
||||
private router: Router,
|
||||
private list: DocumentListViewService
|
||||
) {
|
||||
super(tagService, modalService, TagEditDialogComponent)
|
||||
}
|
||||
|
||||
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit()
|
||||
this.titleService.setTitle(`Tags - ${environment.appTitle}`)
|
||||
}
|
||||
|
||||
getColor(id) {
|
||||
return TAG_COLOURS.find(c => c.id == id)
|
||||
}
|
||||
@@ -31,4 +29,11 @@ export class TagListComponent extends GenericListComponent<PaperlessTag> impleme
|
||||
getObjectName(object: PaperlessTag) {
|
||||
return `tag '${object.name}'`
|
||||
}
|
||||
|
||||
filterDocuments(object: PaperlessTag) {
|
||||
this.list.documentListView.filter_rules = [
|
||||
{rule_type: FILTER_HAS_TAG, value: object.id.toString()}
|
||||
]
|
||||
this.router.navigate(["documents"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
... <span *ngFor="let fragment of highlights">
|
||||
<span *ngFor="let token of fragment" [ngClass]="token.term != null ? 'match term'+ token.term : ''">{{token.text}}</span> ...
|
||||
<span *ngFor="let token of fragment" [class.match]="token.highlight">{{token.text}}</span> ...
|
||||
</span>
|
||||
@@ -1,4 +1,4 @@
|
||||
.match {
|
||||
color: black;
|
||||
background-color: orange;
|
||||
background-color: rgb(255, 211, 66);
|
||||
}
|
||||
@@ -3,7 +3,12 @@
|
||||
|
||||
<div *ngIf="errorMessage" class="alert alert-danger">Invalid search query: {{errorMessage}}</div>
|
||||
|
||||
<p>
|
||||
<p *ngIf="more_like">
|
||||
Showing documents similar to
|
||||
<a routerLink="/documents/{{more_like}}">{{more_like_doc?.original_file_name}}</a>
|
||||
</p>
|
||||
|
||||
<p *ngIf="query">
|
||||
Search string: <i>{{query}}</i>
|
||||
<ng-container *ngIf="correctedQuery">
|
||||
- Did you mean "<a [routerLink]="" (click)="searchCorrectedQuery()">{{correctedQuery}}</a>"?
|
||||
@@ -15,7 +20,9 @@
|
||||
<p>{{resultCount}} result(s)</p>
|
||||
<app-document-card-large *ngFor="let result of results"
|
||||
[document]="result.document"
|
||||
[details]="result.highlights">
|
||||
[details]="result.highlights"
|
||||
[searchScore]="result.score / maxScore"
|
||||
[moreLikeThis]="true">
|
||||
|
||||
</app-document-card-large>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { PaperlessDocument } from 'src/app/data/paperless-document';
|
||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
|
||||
import { SearchHit } from 'src/app/data/search-result';
|
||||
import { DocumentService } from 'src/app/services/rest/document.service';
|
||||
import { SearchService } from 'src/app/services/rest/search.service';
|
||||
import { environment } from 'src/environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'app-search',
|
||||
@@ -16,6 +17,10 @@ export class SearchComponent implements OnInit {
|
||||
|
||||
query: string = ""
|
||||
|
||||
more_like: number
|
||||
|
||||
more_like_doc: PaperlessDocument
|
||||
|
||||
searching = false
|
||||
|
||||
currentPage = 1
|
||||
@@ -28,27 +33,40 @@ export class SearchComponent implements OnInit {
|
||||
|
||||
errorMessage: string
|
||||
|
||||
constructor(private searchService: SearchService, private route: ActivatedRoute, private router: Router, private titleService: Title) { }
|
||||
get maxScore() {
|
||||
return this.results?.length > 0 ? this.results[0].score : 100
|
||||
}
|
||||
|
||||
constructor(private searchService: SearchService, private route: ActivatedRoute, private router: Router, private documentService: DocumentService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.queryParamMap.subscribe(paramMap => {
|
||||
window.scrollTo(0, 0)
|
||||
this.query = paramMap.get('query')
|
||||
this.more_like = paramMap.has('more_like') ? +paramMap.get('more_like') : null
|
||||
if (this.more_like) {
|
||||
this.documentService.get(this.more_like).subscribe(r => {
|
||||
this.more_like_doc = r
|
||||
})
|
||||
} else {
|
||||
this.more_like_doc = null
|
||||
}
|
||||
this.searching = true
|
||||
this.currentPage = 1
|
||||
this.loadPage()
|
||||
this.titleService.setTitle(`Search: ${this.query} - ${environment.appTitle}`)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
searchCorrectedQuery() {
|
||||
this.router.navigate(["search"], {queryParams: {query: this.correctedQuery}})
|
||||
this.router.navigate(["search"], {queryParams: {query: this.correctedQuery, more_like: this.more_like}})
|
||||
}
|
||||
|
||||
loadPage(append: boolean = false) {
|
||||
this.errorMessage = null
|
||||
this.correctedQuery = null
|
||||
this.searchService.search(this.query, this.currentPage).subscribe(result => {
|
||||
|
||||
this.searchService.search(this.query, this.currentPage, this.more_like).subscribe(result => {
|
||||
if (append) {
|
||||
this.results.push(...result.results)
|
||||
} else {
|
||||
|
||||
@@ -22,15 +22,15 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [
|
||||
|
||||
{id: FILTER_TITLE, name: "Title contains", filtervar: "title__icontains", datatype: "string", multi: false, default: ""},
|
||||
{id: FILTER_CONTENT, name: "Content contains", filtervar: "content__icontains", datatype: "string", multi: false, default: ""},
|
||||
|
||||
|
||||
{id: FILTER_ASN, name: "ASN is", filtervar: "archive_serial_number", datatype: "number", multi: false},
|
||||
|
||||
|
||||
{id: FILTER_CORRESPONDENT, name: "Correspondent is", filtervar: "correspondent__id", datatype: "correspondent", multi: false},
|
||||
{id: FILTER_DOCUMENT_TYPE, name: "Document type is", filtervar: "document_type__id", datatype: "document_type", multi: false},
|
||||
|
||||
{id: FILTER_IS_IN_INBOX, name: "Is in Inbox", filtervar: "is_in_inbox", datatype: "boolean", multi: false, default: true},
|
||||
{id: FILTER_HAS_TAG, name: "Has tag", filtervar: "tags__id__all", datatype: "tag", multi: true},
|
||||
{id: FILTER_DOES_NOT_HAVE_TAG, name: "Does not have tag", filtervar: "tags__id__none", datatype: "tag", multi: true},
|
||||
{id: FILTER_IS_IN_INBOX, name: "Is in Inbox", filtervar: "is_in_inbox", datatype: "boolean", multi: false, default: true},
|
||||
{id: FILTER_HAS_TAG, name: "Has tag", filtervar: "tags__id__all", datatype: "tag", multi: true},
|
||||
{id: FILTER_DOES_NOT_HAVE_TAG, name: "Does not have tag", filtervar: "tags__id__none", datatype: "tag", multi: true},
|
||||
{id: FILTER_HAS_ANY_TAG, name: "Has any tag", filtervar: "is_tagged", datatype: "boolean", multi: false, default: true},
|
||||
|
||||
{id: FILTER_CREATED_BEFORE, name: "Created before", filtervar: "created__date__lt", datatype: "date", multi: false},
|
||||
@@ -42,7 +42,7 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [
|
||||
|
||||
{id: FILTER_ADDED_BEFORE, name: "Added before", filtervar: "added__date__lt", datatype: "date", multi: false},
|
||||
{id: FILTER_ADDED_AFTER, name: "Added after", filtervar: "added__date__gt", datatype: "date", multi: false},
|
||||
|
||||
|
||||
{id: FILTER_MODIFIED_BEFORE, name: "Modified before", filtervar: "modified__date__lt", datatype: "date", multi: false},
|
||||
{id: FILTER_MODIFIED_AFTER, name: "Modified after", filtervar: "modified__date__gt", datatype: "date", multi: false},
|
||||
]
|
||||
@@ -54,4 +54,4 @@ export interface FilterRuleType {
|
||||
datatype: string //number, string, boolean, date
|
||||
multi: boolean
|
||||
default?: any
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { FilterRuleType } from './filter-rule-type';
|
||||
|
||||
export function cloneFilterRules(filterRules: FilterRule[]): FilterRule[] {
|
||||
if (filterRules) {
|
||||
let newRules: FilterRule[] = []
|
||||
for (let rule of filterRules) {
|
||||
newRules.push({type: rule.type, value: rule.value})
|
||||
newRules.push({rule_type: rule.rule_type, value: rule.value})
|
||||
}
|
||||
return newRules
|
||||
} else {
|
||||
@@ -13,6 +11,6 @@ export function cloneFilterRules(filterRules: FilterRule[]): FilterRule[] {
|
||||
}
|
||||
|
||||
export interface FilterRule {
|
||||
type: FilterRuleType
|
||||
value: any
|
||||
rule_type: number
|
||||
value: string
|
||||
}
|
||||
18
src-ui/src/app/data/paperless-saved-view.ts
Normal file
18
src-ui/src/app/data/paperless-saved-view.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { FilterRule } from './filter-rule';
|
||||
import { ObjectWithId } from './object-with-id';
|
||||
|
||||
export interface PaperlessSavedView extends ObjectWithId {
|
||||
|
||||
name?: string
|
||||
|
||||
show_on_dashboard?: boolean
|
||||
|
||||
show_in_sidebar?: boolean
|
||||
|
||||
sort_field: string
|
||||
|
||||
sort_reverse: boolean
|
||||
|
||||
filter_rules: FilterRule[]
|
||||
|
||||
}
|
||||
@@ -6,14 +6,14 @@ export const TAG_COLOURS = [
|
||||
{id: 1, value: "#a6cee3", name: "Light Blue", textColor: "#000000"},
|
||||
{id: 2, value: "#1f78b4", name: "Blue", textColor: "#ffffff"},
|
||||
{id: 3, value: "#b2df8a", name: "Light Green", textColor: "#000000"},
|
||||
{id: 4, value: "#33a02c", name: "Green", textColor: "#000000"},
|
||||
{id: 4, value: "#33a02c", name: "Green", textColor: "#ffffff"},
|
||||
{id: 5, value: "#fb9a99", name: "Light Red", textColor: "#000000"},
|
||||
{id: 6, value: "#e31a1c", name: "Red ", textColor: "#ffffff"},
|
||||
{id: 7, value: "#fdbf6f", name: "Light Orange", textColor: "#000000"},
|
||||
{id: 8, value: "#ff7f00", name: "Orange", textColor: "#000000"},
|
||||
{id: 9, value: "#cab2d6", name: "Light Violet", textColor: "#000000"},
|
||||
{id: 10, value: "#6a3d9a", name: "Violet", textColor: "#ffffff"},
|
||||
{id: 11, value: "#b15928", name: "Brown", textColor: "#000000"},
|
||||
{id: 11, value: "#b15928", name: "Brown", textColor: "#ffffff"},
|
||||
{id: 12, value: "#000000", name: "Black", textColor: "#ffffff"},
|
||||
{id: 13, value: "#cccccc", name: "Light Grey", textColor: "#000000"}
|
||||
]
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { FilterRule } from './filter-rule';
|
||||
|
||||
export interface SavedViewConfig {
|
||||
|
||||
id?: string
|
||||
|
||||
filterRules: FilterRule[]
|
||||
|
||||
sortField: string
|
||||
|
||||
sortDirection: string
|
||||
|
||||
title?: string
|
||||
|
||||
showInSideBar?: boolean
|
||||
|
||||
showInDashboard?: boolean
|
||||
|
||||
}
|
||||
@@ -7,16 +7,21 @@ import {
|
||||
} from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { CookieService } from 'ngx-cookie-service';
|
||||
import { Meta } from '@angular/platform-browser';
|
||||
|
||||
@Injectable()
|
||||
export class CsrfInterceptor implements HttpInterceptor {
|
||||
|
||||
constructor(private cookieService: CookieService) {
|
||||
constructor(private cookieService: CookieService, private meta: Meta) {
|
||||
|
||||
}
|
||||
|
||||
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
|
||||
let csrfToken = this.cookieService.get('csrftoken')
|
||||
let prefix = ""
|
||||
if (this.meta.getTag('name=cookie_prefix')) {
|
||||
prefix = this.meta.getTag('name=cookie_prefix').content
|
||||
}
|
||||
let csrfToken = this.cookieService.get(`${prefix?prefix:''}csrftoken`)
|
||||
if (csrfToken) {
|
||||
request = request.clone({
|
||||
setHeaders: {
|
||||
|
||||
8
src-ui/src/app/pipes/document-title.pipe.spec.ts
Normal file
8
src-ui/src/app/pipes/document-title.pipe.spec.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { DocumentTitlePipe } from './document-title.pipe';
|
||||
|
||||
describe('DocumentTitlePipe', () => {
|
||||
it('create an instance', () => {
|
||||
const pipe = new DocumentTitlePipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user