mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-11-03 19:17:13 -05:00 
			
		
		
		
	Merge remote-tracking branch 'upstream/dev' into feature/dark-mode
This commit is contained in:
		
						commit
						2e0d36c4d9
					
				@ -1,4 +1,4 @@
 | 
				
			|||||||
bind = '[::]:8000'
 | 
					bind = ['[::]:8000', 'localhost:8000']
 | 
				
			||||||
backlog = 2048
 | 
					backlog = 2048
 | 
				
			||||||
workers = 3
 | 
					workers = 3
 | 
				
			||||||
worker_class = 'sync'
 | 
					worker_class = 'sync'
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,7 @@ services:
 | 
				
			|||||||
      POSTGRES_PASSWORD: paperless
 | 
					      POSTGRES_PASSWORD: paperless
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  webserver:
 | 
					  webserver:
 | 
				
			||||||
    image: jonaswinkler/paperless-ng:0.9.9
 | 
					    image: jonaswinkler/paperless-ng:0.9.10
 | 
				
			||||||
    restart: always
 | 
					    restart: always
 | 
				
			||||||
    depends_on:
 | 
					    depends_on:
 | 
				
			||||||
      - db
 | 
					      - db
 | 
				
			||||||
 | 
				
			|||||||
@ -5,7 +5,7 @@ services:
 | 
				
			|||||||
    restart: always
 | 
					    restart: always
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  webserver:
 | 
					  webserver:
 | 
				
			||||||
    image: jonaswinkler/paperless-ng:0.9.9
 | 
					    image: jonaswinkler/paperless-ng:0.9.10
 | 
				
			||||||
    restart: always
 | 
					    restart: always
 | 
				
			||||||
    depends_on:
 | 
					    depends_on:
 | 
				
			||||||
      - broker
 | 
					      - broker
 | 
				
			||||||
 | 
				
			|||||||
@ -22,6 +22,7 @@ RUN apt-get update \
 | 
				
			|||||||
		libpq-dev \
 | 
							libpq-dev \
 | 
				
			||||||
		libqpdf-dev \
 | 
							libqpdf-dev \
 | 
				
			||||||
		libxml2 \
 | 
							libxml2 \
 | 
				
			||||||
 | 
							libxslt1-dev \
 | 
				
			||||||
		optipng \
 | 
							optipng \
 | 
				
			||||||
		pngquant \
 | 
							pngquant \
 | 
				
			||||||
		qpdf \
 | 
							qpdf \
 | 
				
			||||||
 | 
				
			|||||||
@ -8,7 +8,7 @@ loglevel=info                ; log level; default info; others: debug,warn,trace
 | 
				
			|||||||
user=root
 | 
					user=root
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[program:gunicorn]
 | 
					[program:gunicorn]
 | 
				
			||||||
command=gunicorn -c /usr/src/paperless/gunicorn.conf.py -b '[::]:8000' paperless.wsgi
 | 
					command=gunicorn -c /usr/src/paperless/gunicorn.conf.py paperless.wsgi
 | 
				
			||||||
user=paperless
 | 
					user=paperless
 | 
				
			||||||
 | 
					
 | 
				
			||||||
stdout_logfile=/dev/stdout
 | 
					stdout_logfile=/dev/stdout
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,35 @@
 | 
				
			|||||||
Changelog
 | 
					Changelog
 | 
				
			||||||
*********
 | 
					*********
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					paperless-ng 0.9.10
 | 
				
			||||||
 | 
					###################
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* Bulk editing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  * Thanks to `Michael Shamoon`_, we've got a new interface for the bulk editor.
 | 
				
			||||||
 | 
					  * There are some configuration options in the settings to alter the behavior.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* Other changes and additions
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  * The Paperless-ng logo now navigates to the dashboard.
 | 
				
			||||||
 | 
					  * Filter for documents that don't have any correspondents, types or tags assigned.
 | 
				
			||||||
 | 
					  * Tags, types and correspondents are now sorted case insensitive.
 | 
				
			||||||
 | 
					  * Lots of preparation work for localization support.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* Fixes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  * Added missing dependencies for Raspberry Pi builds.
 | 
				
			||||||
 | 
					  * Fixed an issue with plain text file consumption: Thumbnail generation failed due to missing fonts.
 | 
				
			||||||
 | 
					  * An issue with the search index reporting missing documents after bulk deletes was fixed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. note::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  The bulk delete operations did not update the search index. Therefore, documents that you deleted remained in the index and
 | 
				
			||||||
 | 
					  caused the search to return messages about missing documents when searching. Further bulk operations will properly update
 | 
				
			||||||
 | 
					  the index.
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  However, this change is not retroactive: If you used the delete method of the bulk editor, you need to reindex your search index
 | 
				
			||||||
 | 
					  by :ref:`running the management command document_index with the argument reindex <administration-index>`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
paperless-ng 0.9.9
 | 
					paperless-ng 0.9.9
 | 
				
			||||||
##################
 | 
					##################
 | 
				
			||||||
 | 
				
			|||||||
@ -400,6 +400,15 @@ PAPERLESS_FILENAME_DATE_ORDER=<format>
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    Defaults to none, which disables this feature.
 | 
					    Defaults to none, which disables this feature.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					PAPERLESS_THUMBNAIL_FONT_NAME=<filename>
 | 
				
			||||||
 | 
					    Paperless creates thumbnails for plain text files by rendering the content
 | 
				
			||||||
 | 
					    of the file on an image and uses a predefined font for that. This
 | 
				
			||||||
 | 
					    font can be changed here.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Note that this won't have any effect on already generated thumbnails.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Defaults to ``/usr/share/fonts/liberation/LiberationSerif-Regular.ttf``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Binaries
 | 
					Binaries
 | 
				
			||||||
########
 | 
					########
 | 
				
			||||||
 | 
				
			|||||||
@ -54,6 +54,7 @@
 | 
				
			|||||||
#PAPERLESS_POST_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh
 | 
					#PAPERLESS_POST_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh
 | 
				
			||||||
#PAPERLESS_FILENAME_DATE_ORDER=YMD
 | 
					#PAPERLESS_FILENAME_DATE_ORDER=YMD
 | 
				
			||||||
#PAPERLESS_FILENAME_PARSE_TRANSFORMS=[]
 | 
					#PAPERLESS_FILENAME_PARSE_TRANSFORMS=[]
 | 
				
			||||||
 | 
					#PAPERLESS_THUMBNAIL_FONT_NAME=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Binaries
 | 
					# Binaries
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -21,10 +21,10 @@ const LAST_YEAR = 3
 | 
				
			|||||||
export class DateDropdownComponent implements OnInit, OnDestroy {
 | 
					export class DateDropdownComponent implements OnInit, OnDestroy {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  quickFilters = [
 | 
					  quickFilters = [
 | 
				
			||||||
    {id: LAST_7_DAYS, name: "Last 7 days"},
 | 
					    {id: LAST_7_DAYS, name: $localize`Last 7 days`},
 | 
				
			||||||
    {id: LAST_MONTH, name: "Last month"},
 | 
					    {id: LAST_MONTH, name: $localize`Last month`},
 | 
				
			||||||
    {id: LAST_3_MONTHS, name: "Last 3 months"},
 | 
					    {id: LAST_3_MONTHS, name: $localize`Last 3 months`},
 | 
				
			||||||
    {id: LAST_YEAR, name: "Last year"}
 | 
					    {id: LAST_YEAR, name: $localize`Last year`}
 | 
				
			||||||
  ]
 | 
					  ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Input()
 | 
					  @Input()
 | 
				
			||||||
 | 
				
			|||||||
@ -142,7 +142,7 @@ export class FilterableDropdownComponent {
 | 
				
			|||||||
    if (items) {
 | 
					    if (items) {
 | 
				
			||||||
      this._selectionModel.items = Array.from(items)
 | 
					      this._selectionModel.items = Array.from(items)
 | 
				
			||||||
      this._selectionModel.items.unshift({
 | 
					      this._selectionModel.items.unshift({
 | 
				
			||||||
        name: "None",
 | 
					        name: $localize`Not assigned`,
 | 
				
			||||||
        id: null
 | 
					        id: null
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -195,6 +195,9 @@ export class FilterableDropdownComponent {
 | 
				
			|||||||
  @Input()
 | 
					  @Input()
 | 
				
			||||||
  editing = false
 | 
					  editing = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  applyOnClose = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Output()
 | 
					  @Output()
 | 
				
			||||||
  apply = new EventEmitter<ChangedItems>()
 | 
					  apply = new EventEmitter<ChangedItems>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -208,7 +211,9 @@ export class FilterableDropdownComponent {
 | 
				
			|||||||
  applyClicked() {
 | 
					  applyClicked() {
 | 
				
			||||||
    if (this.selectionModel.isDirty()) {
 | 
					    if (this.selectionModel.isDirty()) {
 | 
				
			||||||
      this.dropdown.close()
 | 
					      this.dropdown.close()
 | 
				
			||||||
      this.apply.emit(this.selectionModel.diff())
 | 
					      if (!this.applyOnClose) {
 | 
				
			||||||
 | 
					        this.apply.emit(this.selectionModel.diff())
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -223,6 +228,9 @@ export class FilterableDropdownComponent {
 | 
				
			|||||||
      this.open.next()
 | 
					      this.open.next()
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      this.filterText = ''
 | 
					      this.filterText = ''
 | 
				
			||||||
 | 
					      if (this.applyOnClose && this.selectionModel.isDirty()) {
 | 
				
			||||||
 | 
					        this.apply.emit(this.selectionModel.diff())
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -132,7 +132,7 @@
 | 
				
			|||||||
            <div [ngbNavOutlet]="nav" class="mt-2"></div>
 | 
					            <div [ngbNavOutlet]="nav" class="mt-2"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <button type="button" class="btn btn-outline-secondary" (click)="discard()" i18n>Discard</button> 
 | 
					            <button type="button" class="btn btn-outline-secondary" (click)="discard()" i18n>Discard</button> 
 | 
				
			||||||
            <button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()" i18n>Save & edit next</button> 
 | 
					            <button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()" i18n>Save & next</button> 
 | 
				
			||||||
            <button type="submit" class="btn btn-primary" i18n>Save</button> 
 | 
					            <button type="submit" class="btn btn-primary" i18n>Save</button> 
 | 
				
			||||||
        </form>
 | 
					        </form>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -30,6 +30,7 @@
 | 
				
			|||||||
        [items]="tags"
 | 
					        [items]="tags"
 | 
				
			||||||
        [editing]="true"
 | 
					        [editing]="true"
 | 
				
			||||||
        [multiple]="true"
 | 
					        [multiple]="true"
 | 
				
			||||||
 | 
					        [applyOnClose]="applyOnClose"
 | 
				
			||||||
        (open)="openTagsDropdown()"
 | 
					        (open)="openTagsDropdown()"
 | 
				
			||||||
        [(selectionModel)]="tagSelectionModel"
 | 
					        [(selectionModel)]="tagSelectionModel"
 | 
				
			||||||
        (apply)="setTags($event)">
 | 
					        (apply)="setTags($event)">
 | 
				
			||||||
@ -37,6 +38,7 @@
 | 
				
			|||||||
      <app-filterable-dropdown class="mr-2 mr-md-3" title="Correspondent" icon="person-fill"
 | 
					      <app-filterable-dropdown class="mr-2 mr-md-3" title="Correspondent" icon="person-fill"
 | 
				
			||||||
        [items]="correspondents"
 | 
					        [items]="correspondents"
 | 
				
			||||||
        [editing]="true"
 | 
					        [editing]="true"
 | 
				
			||||||
 | 
					        [applyOnClose]="applyOnClose"
 | 
				
			||||||
        (open)="openCorrespondentDropdown()"
 | 
					        (open)="openCorrespondentDropdown()"
 | 
				
			||||||
        [(selectionModel)]="correspondentSelectionModel"
 | 
					        [(selectionModel)]="correspondentSelectionModel"
 | 
				
			||||||
        (apply)="setCorrespondents($event)">
 | 
					        (apply)="setCorrespondents($event)">
 | 
				
			||||||
@ -44,6 +46,7 @@
 | 
				
			|||||||
      <app-filterable-dropdown class="mr-2 mr-md-3" title="Document Type" icon="file-earmark-fill"
 | 
					      <app-filterable-dropdown class="mr-2 mr-md-3" title="Document Type" icon="file-earmark-fill"
 | 
				
			||||||
        [items]="documentTypes"
 | 
					        [items]="documentTypes"
 | 
				
			||||||
        [editing]="true"
 | 
					        [editing]="true"
 | 
				
			||||||
 | 
					        [applyOnClose]="applyOnClose"
 | 
				
			||||||
        (open)="openDocumentTypeDropdown()"
 | 
					        (open)="openDocumentTypeDropdown()"
 | 
				
			||||||
        [(selectionModel)]="documentTypeSelectionModel"
 | 
					        [(selectionModel)]="documentTypeSelectionModel"
 | 
				
			||||||
        (apply)="setDocumentTypes($event)">
 | 
					        (apply)="setDocumentTypes($event)">
 | 
				
			||||||
 | 
				
			|||||||
@ -15,6 +15,7 @@ import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog
 | 
				
			|||||||
import { ChangedItems, FilterableDropdownSelectionModel } from '../../common/filterable-dropdown/filterable-dropdown.component';
 | 
					import { ChangedItems, FilterableDropdownSelectionModel } from '../../common/filterable-dropdown/filterable-dropdown.component';
 | 
				
			||||||
import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component';
 | 
					import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component';
 | 
				
			||||||
import { MatchingModel } from 'src/app/data/matching-model';
 | 
					import { MatchingModel } from 'src/app/data/matching-model';
 | 
				
			||||||
 | 
					import { SettingsService, SETTINGS_KEYS } from 'src/app/services/settings.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Component({
 | 
					@Component({
 | 
				
			||||||
  selector: 'app-bulk-editor',
 | 
					  selector: 'app-bulk-editor',
 | 
				
			||||||
@ -38,9 +39,13 @@ export class BulkEditorComponent {
 | 
				
			|||||||
    public list: DocumentListViewService,
 | 
					    public list: DocumentListViewService,
 | 
				
			||||||
    private documentService: DocumentService,
 | 
					    private documentService: DocumentService,
 | 
				
			||||||
    private modalService: NgbModal,
 | 
					    private modalService: NgbModal,
 | 
				
			||||||
    private openDocumentService: OpenDocumentsService
 | 
					    private openDocumentService: OpenDocumentsService,
 | 
				
			||||||
 | 
					    private settings: SettingsService
 | 
				
			||||||
  ) { }
 | 
					  ) { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  applyOnClose: boolean = this.settings.get(SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE)
 | 
				
			||||||
 | 
					  showConfirmationDialogs: boolean = this.settings.get(SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ngOnInit() {
 | 
					  ngOnInit() {
 | 
				
			||||||
    this.tagService.listAll().subscribe(result => this.tags = result.results)
 | 
					    this.tagService.listAll().subscribe(result => this.tags = result.results)
 | 
				
			||||||
    this.correspondentService.listAll().subscribe(result => this.correspondents = result.results)
 | 
					    this.correspondentService.listAll().subscribe(result => this.correspondents = result.results)
 | 
				
			||||||
@ -51,10 +56,10 @@ export class BulkEditorComponent {
 | 
				
			|||||||
    return this.documentService.bulkEdit(Array.from(this.list.selected), method, args).pipe(
 | 
					    return this.documentService.bulkEdit(Array.from(this.list.selected), method, args).pipe(
 | 
				
			||||||
      tap(() => {
 | 
					      tap(() => {
 | 
				
			||||||
        this.list.reload()
 | 
					        this.list.reload()
 | 
				
			||||||
 | 
					        this.list.reduceSelectionToFilter()
 | 
				
			||||||
        this.list.selected.forEach(id => {
 | 
					        this.list.selected.forEach(id => {
 | 
				
			||||||
          this.openDocumentService.refreshDocument(id)
 | 
					          this.openDocumentService.refreshDocument(id)
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        this.list.selectNone()
 | 
					 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -105,30 +110,39 @@ export class BulkEditorComponent {
 | 
				
			|||||||
  setTags(changedTags: ChangedItems) {
 | 
					  setTags(changedTags: ChangedItems) {
 | 
				
			||||||
    if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length == 0) return
 | 
					    if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length == 0) return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
 | 
					    if (this.showConfirmationDialogs) {
 | 
				
			||||||
    modal.componentInstance.title = $localize`Confirm tags assignment`
 | 
					      let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
 | 
				
			||||||
    if (changedTags.itemsToAdd.length == 1 && changedTags.itemsToRemove.length == 0) {
 | 
					      modal.componentInstance.title = $localize`Confirm tags assignment`
 | 
				
			||||||
      let tag = changedTags.itemsToAdd[0]
 | 
					      if (changedTags.itemsToAdd.length == 1 && changedTags.itemsToRemove.length == 0) {
 | 
				
			||||||
      modal.componentInstance.message = $localize`This operation will add the tag ${tag.name} to all ${this.list.selected.size} selected document(s).`
 | 
					        let tag = changedTags.itemsToAdd[0]
 | 
				
			||||||
    } else if (changedTags.itemsToAdd.length > 1 && changedTags.itemsToRemove.length == 0) {
 | 
					        modal.componentInstance.message = $localize`This operation will add the tag ${tag.name} to all ${this.list.selected.size} selected document(s).`
 | 
				
			||||||
      modal.componentInstance.message = $localize`This operation will add the tags ${this._localizeList(changedTags.itemsToAdd)} to all ${this.list.selected.size} selected document(s).`
 | 
					      } else if (changedTags.itemsToAdd.length > 1 && changedTags.itemsToRemove.length == 0) {
 | 
				
			||||||
    } else if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length == 1) {
 | 
					        modal.componentInstance.message = $localize`This operation will add the tags ${this._localizeList(changedTags.itemsToAdd)} to all ${this.list.selected.size} selected document(s).`
 | 
				
			||||||
      let tag = changedTags.itemsToAdd[0]
 | 
					      } else if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length == 1) {
 | 
				
			||||||
      modal.componentInstance.message = $localize`This operation will remove the tag ${tag.name} from all ${this.list.selected.size} selected document(s).`
 | 
					        let tag = changedTags.itemsToRemove[0]
 | 
				
			||||||
    } else if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length > 1) {
 | 
					        modal.componentInstance.message = $localize`This operation will remove the tag ${tag.name} from all ${this.list.selected.size} selected document(s).`
 | 
				
			||||||
      modal.componentInstance.message = $localize`This operation will remove the tags ${this._localizeList(changedTags.itemsToRemove)} from all ${this.list.selected.size} selected document(s).`
 | 
					      } else if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length > 1) {
 | 
				
			||||||
    } else {
 | 
					        modal.componentInstance.message = $localize`This operation will remove the tags ${this._localizeList(changedTags.itemsToRemove)} from all ${this.list.selected.size} selected document(s).`
 | 
				
			||||||
      modal.componentInstance.message = $localize`This operation will add the tags ${this._localizeList(changedTags.itemsToAdd)} and remove the tags ${this._localizeList(changedTags.itemsToRemove)} on all ${this.list.selected.size} selected document(s).`
 | 
					      } else {
 | 
				
			||||||
    }
 | 
					        modal.componentInstance.message = $localize`This operation will add the tags ${this._localizeList(changedTags.itemsToAdd)} and remove the tags ${this._localizeList(changedTags.itemsToRemove)} on all ${this.list.selected.size} selected document(s).`
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
    modal.componentInstance.btnClass = "btn-warning"
 | 
					      modal.componentInstance.btnClass = "btn-warning"
 | 
				
			||||||
    modal.componentInstance.btnCaption = $localize`Confirm`
 | 
					      modal.componentInstance.btnCaption = $localize`Confirm`
 | 
				
			||||||
    modal.componentInstance.confirmClicked.subscribe(() => {
 | 
					      modal.componentInstance.confirmClicked.subscribe(() => {
 | 
				
			||||||
      this.executeBulkOperation('modify_tags', {"add_tags": changedTags.itemsToAdd.map(t => t.id), "remove_tags": changedTags.itemsToRemove.map(t => t.id)}).subscribe(
 | 
					        this.performSetTags(modal, changedTags)
 | 
				
			||||||
        response => {
 | 
					      })
 | 
				
			||||||
          this.tagService.clearCache()
 | 
					    } else {
 | 
				
			||||||
 | 
					      this.performSetTags(null, changedTags)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private performSetTags(modal, changedTags: ChangedItems) {
 | 
				
			||||||
 | 
					    this.executeBulkOperation('modify_tags', {"add_tags": changedTags.itemsToAdd.map(t => t.id), "remove_tags": changedTags.itemsToRemove.map(t => t.id)}).subscribe(
 | 
				
			||||||
 | 
					      response => {
 | 
				
			||||||
 | 
					        if (modal) {
 | 
				
			||||||
          modal.close()
 | 
					          modal.close()
 | 
				
			||||||
        })
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -136,47 +150,67 @@ export class BulkEditorComponent {
 | 
				
			|||||||
  setCorrespondents(changedCorrespondents: ChangedItems) {
 | 
					  setCorrespondents(changedCorrespondents: ChangedItems) {
 | 
				
			||||||
    if (changedCorrespondents.itemsToAdd.length == 0 && changedCorrespondents.itemsToRemove.length == 0) return
 | 
					    if (changedCorrespondents.itemsToAdd.length == 0 && changedCorrespondents.itemsToRemove.length == 0) return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
 | 
					 | 
				
			||||||
    modal.componentInstance.title = $localize`Confirm correspondent assignment`
 | 
					 | 
				
			||||||
    let correspondent = changedCorrespondents.itemsToAdd.length > 0 ? changedCorrespondents.itemsToAdd[0] : null
 | 
					    let correspondent = changedCorrespondents.itemsToAdd.length > 0 ? changedCorrespondents.itemsToAdd[0] : null
 | 
				
			||||||
    if (correspondent) {
 | 
					
 | 
				
			||||||
      modal.componentInstance.message = $localize`This operation will assign the correspondent ${correspondent.name} to all ${this.list.selected.size} selected document(s).`
 | 
					    if (this.showConfirmationDialogs) {
 | 
				
			||||||
 | 
					      let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
 | 
				
			||||||
 | 
					      modal.componentInstance.title = $localize`Confirm correspondent assignment`
 | 
				
			||||||
 | 
					      if (correspondent) {
 | 
				
			||||||
 | 
					        modal.componentInstance.message = $localize`This operation will assign the correspondent ${correspondent.name} to all ${this.list.selected.size} selected document(s).`
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        modal.componentInstance.message = $localize`This operation will remove the correspondent from all ${this.list.selected.size} selected document(s).`
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      modal.componentInstance.btnClass = "btn-warning"
 | 
				
			||||||
 | 
					      modal.componentInstance.btnCaption = $localize`Confirm`
 | 
				
			||||||
 | 
					      modal.componentInstance.confirmClicked.subscribe(() => {
 | 
				
			||||||
 | 
					        this.performSetCorrespondents(modal, correspondent)
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      modal.componentInstance.message = $localize`This operation will remove the correspondent from all ${this.list.selected.size} selected document(s).`
 | 
					      this.performSetCorrespondents(null, correspondent)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    modal.componentInstance.btnClass = "btn-warning"
 | 
					  }
 | 
				
			||||||
    modal.componentInstance.btnCaption = $localize`Confirm`
 | 
					
 | 
				
			||||||
    modal.componentInstance.confirmClicked.subscribe(() => {
 | 
					  private performSetCorrespondents(modal, correspondent: MatchingModel) {
 | 
				
			||||||
      this.executeBulkOperation('set_correspondent', {"correspondent": correspondent?.id}).subscribe(
 | 
					    this.executeBulkOperation('set_correspondent', {"correspondent": correspondent ? correspondent.id : null}).subscribe(
 | 
				
			||||||
        response => {
 | 
					      response => {
 | 
				
			||||||
          this.correspondentService.clearCache()
 | 
					        if (modal) {
 | 
				
			||||||
          modal.close()
 | 
					          modal.close()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      )
 | 
					      }
 | 
				
			||||||
    })
 | 
					    )
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  setDocumentTypes(changedDocumentTypes: ChangedItems) {
 | 
					  setDocumentTypes(changedDocumentTypes: ChangedItems) {
 | 
				
			||||||
    if (changedDocumentTypes.itemsToAdd.length == 0 && changedDocumentTypes.itemsToRemove.length == 0) return
 | 
					    if (changedDocumentTypes.itemsToAdd.length == 0 && changedDocumentTypes.itemsToRemove.length == 0) return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
 | 
					 | 
				
			||||||
    modal.componentInstance.title = $localize`Confirm document type assignment`
 | 
					 | 
				
			||||||
    let documentType = changedDocumentTypes.itemsToAdd.length > 0 ? changedDocumentTypes.itemsToAdd[0] : null
 | 
					    let documentType = changedDocumentTypes.itemsToAdd.length > 0 ? changedDocumentTypes.itemsToAdd[0] : null
 | 
				
			||||||
    if (documentType) {
 | 
					
 | 
				
			||||||
      modal.componentInstance.message = $localize`This operation will assign the document type ${documentType.name} to all ${this.list.selected.size} selected document(s).`
 | 
					    if (this.showConfirmationDialogs) {
 | 
				
			||||||
 | 
					      let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
 | 
				
			||||||
 | 
					      modal.componentInstance.title = $localize`Confirm document type assignment`
 | 
				
			||||||
 | 
					      if (documentType) {
 | 
				
			||||||
 | 
					        modal.componentInstance.message = $localize`This operation will assign the document type ${documentType.name} to all ${this.list.selected.size} selected document(s).`
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        modal.componentInstance.message = $localize`This operation will remove the document type from all ${this.list.selected.size} selected document(s).`
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      modal.componentInstance.btnClass = "btn-warning"
 | 
				
			||||||
 | 
					      modal.componentInstance.btnCaption = $localize`Confirm`
 | 
				
			||||||
 | 
					      modal.componentInstance.confirmClicked.subscribe(() => {
 | 
				
			||||||
 | 
					        this.performSetDocumentTypes(modal, documentType)
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      modal.componentInstance.message = $localize`This operation will remove the document type from all ${this.list.selected.size} selected document(s).`
 | 
					      this.performSetDocumentTypes(null, documentType)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    modal.componentInstance.btnClass = "btn-warning"
 | 
					  }
 | 
				
			||||||
    modal.componentInstance.btnCaption = $localize`Confirm`
 | 
					
 | 
				
			||||||
    modal.componentInstance.confirmClicked.subscribe(() => {
 | 
					  private performSetDocumentTypes(modal, documentType) {
 | 
				
			||||||
      this.executeBulkOperation('set_document_type', {"document_type": documentType?.id}).subscribe(
 | 
					    this.executeBulkOperation('set_document_type', {"document_type": documentType ? documentType.id : null}).subscribe(
 | 
				
			||||||
        response => {
 | 
					      response => {
 | 
				
			||||||
          this.documentService.clearCache()
 | 
					        if (modal) {
 | 
				
			||||||
          modal.close()
 | 
					          modal.close()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      )
 | 
					      }
 | 
				
			||||||
    })
 | 
					    )
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  applyDelete() {
 | 
					  applyDelete() {
 | 
				
			||||||
 | 
				
			|||||||
@ -84,7 +84,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<div class="d-flex justify-content-between align-items-center">
 | 
					<div class="d-flex justify-content-between align-items-center">
 | 
				
			||||||
  <p i18n *ngIf="list.selected.size > 0">Selected {{list.selected.size}} of {{list.collectionSize || 0}} {list.collectionSize, plural, =1 {document} other {documents}}</p>
 | 
					  <p i18n *ngIf="list.selected.size > 0">Selected {{list.selected.size}} of {{list.collectionSize || 0}} {list.collectionSize, plural, =1 {document} other {documents}}</p>
 | 
				
			||||||
  <p i18n *ngIf="list.selected.size == 0">{{list.collectionSize || 0}} {list.collectionSize, plural, =1 {document} other {documents}}</p>
 | 
					  <p *ngIf="list.selected.size == 0">{list.collectionSize, plural, =1 {1 document} other {{{list.collectionSize || 0}} documents}}</p>
 | 
				
			||||||
  <ngb-pagination [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
 | 
					  <ngb-pagination [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
 | 
				
			||||||
  [rotate]="true" (pageChange)="list.reload()" aria-label="Default pagination"></ngb-pagination>
 | 
					  [rotate]="true" (pageChange)="list.reload()" aria-label="Default pagination"></ngb-pagination>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
				
			|||||||
@ -62,6 +62,10 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @Input()
 | 
					  @Input()
 | 
				
			||||||
  set filterRules (value: FilterRule[]) {
 | 
					  set filterRules (value: FilterRule[]) {
 | 
				
			||||||
 | 
					    this.documentTypeSelectionModel.clear(false)
 | 
				
			||||||
 | 
					    this.tagSelectionModel.clear(false)
 | 
				
			||||||
 | 
					    this.correspondentSelectionModel.clear(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    value.forEach(rule => {
 | 
					    value.forEach(rule => {
 | 
				
			||||||
      switch (rule.rule_type) {
 | 
					      switch (rule.rule_type) {
 | 
				
			||||||
        case FILTER_TITLE:
 | 
					        case FILTER_TITLE:
 | 
				
			||||||
@ -80,22 +84,22 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
 | 
				
			|||||||
          this.dateAddedBefore = rule.value
 | 
					          this.dateAddedBefore = rule.value
 | 
				
			||||||
          break
 | 
					          break
 | 
				
			||||||
        case FILTER_HAS_TAG:
 | 
					        case FILTER_HAS_TAG:
 | 
				
			||||||
          this.tagSelectionModel.set(+rule.value, ToggleableItemState.Selected, false)
 | 
					          this.tagSelectionModel.set(rule.value ? +rule.value : null, ToggleableItemState.Selected, false)
 | 
				
			||||||
 | 
					          break
 | 
				
			||||||
 | 
					        case FILTER_HAS_ANY_TAG:
 | 
				
			||||||
 | 
					          this.tagSelectionModel.set(null, ToggleableItemState.Selected, false)
 | 
				
			||||||
          break
 | 
					          break
 | 
				
			||||||
        case FILTER_CORRESPONDENT:
 | 
					        case FILTER_CORRESPONDENT:
 | 
				
			||||||
          this.correspondentSelectionModel.set(+rule.value, ToggleableItemState.Selected, false)
 | 
					          this.correspondentSelectionModel.set(rule.value ? +rule.value : null, ToggleableItemState.Selected, false)
 | 
				
			||||||
          break
 | 
					          break
 | 
				
			||||||
        case FILTER_DOCUMENT_TYPE:
 | 
					        case FILTER_DOCUMENT_TYPE:
 | 
				
			||||||
          this.documentTypeSelectionModel.set(+rule.value, ToggleableItemState.Selected, false)
 | 
					          this.documentTypeSelectionModel.set(rule.value ? +rule.value : null, ToggleableItemState.Selected, false)
 | 
				
			||||||
          break
 | 
					          break
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Output()
 | 
					  get filterRules() {
 | 
				
			||||||
  filterRulesChange = new EventEmitter<FilterRule[]>()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  updateRules() {
 | 
					 | 
				
			||||||
    let filterRules: FilterRule[] = []
 | 
					    let filterRules: FilterRule[] = []
 | 
				
			||||||
    if (this._titleFilter) {
 | 
					    if (this._titleFilter) {
 | 
				
			||||||
      filterRules.push({rule_type: FILTER_TITLE, value: this._titleFilter})
 | 
					      filterRules.push({rule_type: FILTER_TITLE, value: this._titleFilter})
 | 
				
			||||||
@ -125,7 +129,14 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
 | 
				
			|||||||
    if (this.dateAddedAfter) {
 | 
					    if (this.dateAddedAfter) {
 | 
				
			||||||
      filterRules.push({rule_type: FILTER_ADDED_AFTER, value: this.dateAddedAfter})
 | 
					      filterRules.push({rule_type: FILTER_ADDED_AFTER, value: this.dateAddedAfter})
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    this.filterRulesChange.next(filterRules)
 | 
					    return filterRules
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Output()
 | 
				
			||||||
 | 
					  filterRulesChange = new EventEmitter<FilterRule[]>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  updateRules() {
 | 
				
			||||||
 | 
					    this.filterRulesChange.next(this.filterRules)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  hasFilters() {
 | 
					  hasFilters() {
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,7 @@
 | 
				
			|||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
  <div class="modal-body">
 | 
					  <div class="modal-body">
 | 
				
			||||||
    <app-input-text i18n-title title="Name" formControlName="name"></app-input-text>
 | 
					    <app-input-text i18n-title title="Name" formControlName="name"></app-input-text>
 | 
				
			||||||
    <app-input-check i18n-title title="Show in side bar" formControlName="showInSideBar"></app-input-check>
 | 
					    <app-input-check i18n-title title="Show in sidebar" formControlName="showInSideBar"></app-input-check>
 | 
				
			||||||
    <app-input-check i18n-title title="Show on dashboard" formControlName="showOnDashboard"></app-input-check>
 | 
					    <app-input-check i18n-title title="Show on dashboard" formControlName="showOnDashboard"></app-input-check>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
  <div class="modal-footer">
 | 
					  <div class="modal-footer">
 | 
				
			||||||
 | 
				
			|||||||
@ -9,8 +9,8 @@
 | 
				
			|||||||
    
 | 
					    
 | 
				
			||||||
    <app-input-text i18n-title title="Name" formControlName="name"></app-input-text>
 | 
					    <app-input-text i18n-title title="Name" formControlName="name"></app-input-text>
 | 
				
			||||||
    <app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
 | 
					    <app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
 | 
				
			||||||
    <app-input-text i18n-title title="Match" formControlName="match" hint="Auto matching does not require you to fill in this field."></app-input-text>
 | 
					    <app-input-text i18n-title title="Match" formControlName="match" i18n-hint hint="Auto matching does not require you to fill in this field."></app-input-text>
 | 
				
			||||||
    <app-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" hint="Auto matching ignores this option."></app-input-check>
 | 
					    <app-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" i18n-hint hint="Auto matching ignores this option."></app-input-check>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
  <div class="modal-footer">
 | 
					  <div class="modal-footer">
 | 
				
			||||||
    <button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n>Cancel</button>
 | 
					    <button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n>Cancel</button>
 | 
				
			||||||
 | 
				
			|||||||
@ -9,8 +9,8 @@
 | 
				
			|||||||
      
 | 
					      
 | 
				
			||||||
      <app-input-text i18n-title title="Name" formControlName="name"></app-input-text>
 | 
					      <app-input-text i18n-title title="Name" formControlName="name"></app-input-text>
 | 
				
			||||||
      <app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
 | 
					      <app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
 | 
				
			||||||
      <app-input-text i18n-title title="Match" formControlName="match" hint="Auto matching does not require you to fill in this field."></app-input-text>
 | 
					      <app-input-text i18n-title title="Match" formControlName="match" i18n-hint hint="Auto matching does not require you to fill in this field."></app-input-text>
 | 
				
			||||||
      <app-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" hint="Auto matching ignores this option."></app-input-check>
 | 
					      <app-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" i18n-hint hint="Auto matching ignores this option."></app-input-check>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="modal-footer">
 | 
					    <div class="modal-footer">
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
<app-page-header title="Document types">
 | 
					<app-page-header title="Document types" i18n-title>
 | 
				
			||||||
  <button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" i18n>Create</button>
 | 
					  <button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" i18n>Create</button>
 | 
				
			||||||
</app-page-header>
 | 
					</app-page-header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -39,8 +39,15 @@
 | 
				
			|||||||
              <label class="custom-control-label" for="darkModeEnabled">Enabled</label>
 | 
					              <label class="custom-control-label" for="darkModeEnabled">Enabled</label>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <h4 i18n>Bulk editing</h4>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <app-input-check i18n-title title="Show confirmation dialogs" formControlName="bulkEditConfirmationDialogs" i18n-hint hint="Deleting documents will always ask for confirmation."></app-input-check>
 | 
				
			||||||
 | 
					        <app-input-check i18n-title title="Apply on close" formControlName="bulkEditApplyOnClose"></app-input-check>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      </ng-template>
 | 
					      </ng-template>
 | 
				
			||||||
    </li>
 | 
					    </li>
 | 
				
			||||||
    <li [ngbNavItem]="2">
 | 
					    <li [ngbNavItem]="2">
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,9 @@
 | 
				
			|||||||
import { Component, OnInit, Renderer2  } from '@angular/core';
 | 
					import { Component, OnInit, Renderer2  } from '@angular/core';
 | 
				
			||||||
import { FormControl, FormGroup } from '@angular/forms';
 | 
					import { FormControl, FormGroup } from '@angular/forms';
 | 
				
			||||||
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
 | 
					import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
 | 
				
			||||||
import { GENERAL_SETTINGS } from 'src/app/data/storage-keys';
 | 
					 | 
				
			||||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
 | 
					import { DocumentListViewService } from 'src/app/services/document-list-view.service';
 | 
				
			||||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
 | 
					import { SavedViewService } from 'src/app/services/rest/saved-view.service';
 | 
				
			||||||
 | 
					import { SettingsService, SETTINGS_KEYS } from 'src/app/services/settings.service';
 | 
				
			||||||
import { ToastService } from 'src/app/services/toast.service';
 | 
					import { ToastService } from 'src/app/services/toast.service';
 | 
				
			||||||
import { AppViewService } from 'src/app/services/app-view.service';
 | 
					import { AppViewService } from 'src/app/services/app-view.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -17,13 +17,11 @@ export class SettingsComponent implements OnInit {
 | 
				
			|||||||
  savedViewGroup = new FormGroup({})
 | 
					  savedViewGroup = new FormGroup({})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  settingsForm = new FormGroup({
 | 
					  settingsForm = new FormGroup({
 | 
				
			||||||
    'documentListItemPerPage': new FormControl(+localStorage.getItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE) || GENERAL_SETTINGS.DOCUMENT_LIST_SIZE_DEFAULT),
 | 
					    'bulkEditConfirmationDialogs': new FormControl(this.settings.get(SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS)),
 | 
				
			||||||
    'darkModeUseSystem': new FormControl(
 | 
					    'bulkEditApplyOnClose': new FormControl(this.settings.get(SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE)),
 | 
				
			||||||
      localStorage.getItem(GENERAL_SETTINGS.DARK_MODE_USE_SYSTEM) == undefined ? GENERAL_SETTINGS.DARK_MODE_USE_SYSTEM_DEFAULT : JSON.parse(localStorage.getItem(GENERAL_SETTINGS.DARK_MODE_USE_SYSTEM))
 | 
					    'documentListItemPerPage': new FormControl(this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)),
 | 
				
			||||||
    ),
 | 
					    'darkModeUseSystem': new FormControl(this.settings.get(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM)),
 | 
				
			||||||
    'darkModeEnabled': new FormControl(
 | 
					    'darkModeEnabled': new FormControl(this.settings.get(SETTINGS_KEYS.DARK_MODE_ENABLED)),
 | 
				
			||||||
      localStorage.getItem(GENERAL_SETTINGS.DARK_MODE_ENABLED) == undefined ? GENERAL_SETTINGS.DARK_MODE_ENABLED_DEFAULT : JSON.parse(localStorage.getItem(GENERAL_SETTINGS.DARK_MODE_ENABLED))
 | 
					 | 
				
			||||||
    ),
 | 
					 | 
				
			||||||
    'savedViews': this.savedViewGroup
 | 
					    'savedViews': this.savedViewGroup
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -33,6 +31,7 @@ export class SettingsComponent implements OnInit {
 | 
				
			|||||||
    public savedViewService: SavedViewService,
 | 
					    public savedViewService: SavedViewService,
 | 
				
			||||||
    private documentListViewService: DocumentListViewService,
 | 
					    private documentListViewService: DocumentListViewService,
 | 
				
			||||||
    private toastService: ToastService,
 | 
					    private toastService: ToastService,
 | 
				
			||||||
 | 
					    private settings: SettingsService,
 | 
				
			||||||
    private appViewService: AppViewService
 | 
					    private appViewService: AppViewService
 | 
				
			||||||
  ) { }
 | 
					  ) { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -67,9 +66,11 @@ export class SettingsComponent implements OnInit {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private saveLocalSettings() {
 | 
					  private saveLocalSettings() {
 | 
				
			||||||
    localStorage.setItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE, this.settingsForm.value.documentListItemPerPage)
 | 
					    this.settings.set(SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE, this.settingsForm.value.bulkEditApplyOnClose)
 | 
				
			||||||
    localStorage.setItem(GENERAL_SETTINGS.DARK_MODE_USE_SYSTEM, this.settingsForm.value.darkModeUseSystem)
 | 
					    this.settings.set(SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS, this.settingsForm.value.bulkEditConfirmationDialogs)
 | 
				
			||||||
    localStorage.setItem(GENERAL_SETTINGS.DARK_MODE_ENABLED, (this.settingsForm.value.darkModeEnabled == true).toString())
 | 
					    this.settings.set(SETTINGS_KEYS.DOCUMENT_LIST_SIZE, this.settingsForm.value.documentListItemPerPage)
 | 
				
			||||||
 | 
					    this.settings.set(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM, this.settingsForm.value.darkModeUseSystem)
 | 
				
			||||||
 | 
					    this.settings.set(SETTINGS_KEYS.DARK_MODE_ENABLED, (this.settingsForm.value.darkModeEnabled == true).toString())
 | 
				
			||||||
    this.documentListViewService.updatePageSize()
 | 
					    this.documentListViewService.updatePageSize()
 | 
				
			||||||
    this.appViewService.updateDarkModeSettings()
 | 
					    this.appViewService.updateDarkModeSettings()
 | 
				
			||||||
    this.toastService.showInfo($localize`Settings saved successfully.`)
 | 
					    this.toastService.showInfo($localize`Settings saved successfully.`)
 | 
				
			||||||
 | 
				
			|||||||
@ -10,7 +10,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div class="form-group paperless-input-select">
 | 
					      <div class="form-group paperless-input-select">
 | 
				
			||||||
        <label for="colour">Colour</label>
 | 
					        <label for="colour" i18n>Color</label>
 | 
				
			||||||
        <ng-select name="colour" formControlName="colour" [items]="getColours()" bindValue="id" bindLabel="name" [clearable]="false">
 | 
					        <ng-select name="colour" formControlName="colour" [items]="getColours()" bindValue="id" bindLabel="name" [clearable]="false">
 | 
				
			||||||
          <ng-template ng-option-tmp ng-label-tmp let-item="item">
 | 
					          <ng-template ng-option-tmp ng-label-tmp let-item="item">
 | 
				
			||||||
            <span class="badge" [style.background]="item.value" [style.color]="item.textColor">{{item.name}}</span>
 | 
					            <span class="badge" [style.background]="item.value" [style.color]="item.textColor">{{item.name}}</span>
 | 
				
			||||||
@ -18,13 +18,13 @@
 | 
				
			|||||||
        </ng-select>
 | 
					        </ng-select>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
     
 | 
					     
 | 
				
			||||||
      <app-input-check title="Inbox tag" formControlName="is_inbox_tag" hint="Inbox tags are automatically assigned to all consumed documents."></app-input-check>
 | 
					      <app-input-check i18n-title title="Inbox tag" formControlName="is_inbox_tag" i18n-hint hint="Inbox tags are automatically assigned to all consumed documents."></app-input-check>
 | 
				
			||||||
      <app-input-select title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
 | 
					      <app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
 | 
				
			||||||
      <app-input-text title="Match" formControlName="match" hint="Auto matching does not require you to fill in this field."></app-input-text>
 | 
					      <app-input-text i18n-title title="Match" formControlName="match" i18n-hint hint="Auto matching does not require you to fill in this field."></app-input-text>
 | 
				
			||||||
      <app-input-check title="Case insensitive" formControlName="is_insensitive" hint="Auto matching ignores this option."></app-input-check>
 | 
					      <app-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" i18n-hint hint="Auto matching ignores this option."></app-input-check>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="modal-footer">
 | 
					    <div class="modal-footer">
 | 
				
			||||||
      <button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button>
 | 
					      <button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n>Cancel</button>
 | 
				
			||||||
      <button type="submit" class="btn btn-primary">Save</button>
 | 
					      <button type="submit" class="btn btn-primary" i18n>Save</button>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </form>
 | 
					  </form>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,5 @@
 | 
				
			|||||||
<app-page-header title="Tags">
 | 
					<app-page-header title="Tags" i18n-title>
 | 
				
			||||||
  <button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()">
 | 
					  <button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" i18n>Create</button>
 | 
				
			||||||
    Create
 | 
					 | 
				
			||||||
  </button>
 | 
					 | 
				
			||||||
</app-page-header>
 | 
					</app-page-header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="row m-0 justify-content-end">
 | 
					<div class="row m-0 justify-content-end">
 | 
				
			||||||
@ -12,11 +10,11 @@
 | 
				
			|||||||
<table class="table table-striped border shadow-sm">
 | 
					<table class="table table-striped border shadow-sm">
 | 
				
			||||||
  <thead>
 | 
					  <thead>
 | 
				
			||||||
    <tr>
 | 
					    <tr>
 | 
				
			||||||
      <th scope="col" sortable="name" (sort)="onSort($event)">Name</th>
 | 
					      <th scope="col" sortable="name" (sort)="onSort($event)" i18n>Name</th>
 | 
				
			||||||
      <th scope="col">Colour</th>
 | 
					      <th scope="col" i18n>Color</th>
 | 
				
			||||||
      <th scope="col" sortable="matching_algorithm" (sort)="onSort($event)">Matching</th>
 | 
					      <th scope="col" sortable="matching_algorithm" (sort)="onSort($event)" i18n>Matching</th>
 | 
				
			||||||
      <th scope="col" sortable="document_count" (sort)="onSort($event)">Document count</th>
 | 
					      <th scope="col" sortable="document_count" (sort)="onSort($event)" i18n>Document count</th>
 | 
				
			||||||
      <th scope="col">Actions</th>
 | 
					      <th scope="col" i18n>Actions</th>
 | 
				
			||||||
    </tr>
 | 
					    </tr>
 | 
				
			||||||
  </thead>
 | 
					  </thead>
 | 
				
			||||||
  <tbody>
 | 
					  <tbody>
 | 
				
			||||||
@ -31,21 +29,18 @@
 | 
				
			|||||||
          <button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(tag)">
 | 
					          <button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(tag)">
 | 
				
			||||||
            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-funnel" viewBox="0 0 16 16">
 | 
					            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-funnel" viewBox="0 0 16 16">
 | 
				
			||||||
              <path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2h-11z"/>
 | 
					              <path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2h-11z"/>
 | 
				
			||||||
            </svg>
 | 
					            </svg> <ng-container i18n>Documents</ng-container>
 | 
				
			||||||
            Documents
 | 
					 | 
				
			||||||
          </button>
 | 
					          </button>
 | 
				
			||||||
          <button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(tag)">
 | 
					          <button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(tag)">
 | 
				
			||||||
            <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
 | 
					            <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
 | 
				
			||||||
              <path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
 | 
					              <path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
 | 
				
			||||||
            </svg>
 | 
					            </svg> <ng-container i18n>Edit</ng-container>
 | 
				
			||||||
            Edit
 | 
					 | 
				
			||||||
          </button>
 | 
					          </button>
 | 
				
			||||||
          <button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(tag)">
 | 
					          <button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(tag)">
 | 
				
			||||||
            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
 | 
					            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
 | 
				
			||||||
              <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
 | 
					              <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
 | 
				
			||||||
              <path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
 | 
					              <path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
 | 
				
			||||||
            </svg>
 | 
					            </svg> <ng-container i18n>Delete</ng-container>
 | 
				
			||||||
            Delete
 | 
					 | 
				
			||||||
          </button>
 | 
					          </button>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </td>
 | 
					      </td>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
<app-page-header title="Search results">
 | 
					<app-page-header i18n-title title="Search results">
 | 
				
			||||||
</app-page-header>
 | 
					</app-page-header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div *ngIf="errorMessage" class="alert alert-danger" i18n>Invalid search query: {{errorMessage}}</div>
 | 
					<div *ngIf="errorMessage" class="alert alert-danger" i18n>Invalid search query: {{errorMessage}}</div>
 | 
				
			||||||
 | 
				
			|||||||
@ -5,12 +5,3 @@ export const OPEN_DOCUMENT_SERVICE = {
 | 
				
			|||||||
export const DOCUMENT_LIST_SERVICE = {
 | 
					export const DOCUMENT_LIST_SERVICE = {
 | 
				
			||||||
  CURRENT_VIEW_CONFIG: 'document-list-service:currentViewConfig'
 | 
					  CURRENT_VIEW_CONFIG: 'document-list-service:currentViewConfig'
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
export const GENERAL_SETTINGS = {
 | 
					 | 
				
			||||||
  DOCUMENT_LIST_SIZE: 'general-settings:documentListSize',
 | 
					 | 
				
			||||||
  DOCUMENT_LIST_SIZE_DEFAULT: 50,
 | 
					 | 
				
			||||||
  DARK_MODE_USE_SYSTEM: 'general-settings:darkModeUseSystem',
 | 
					 | 
				
			||||||
  DARK_MODE_USE_SYSTEM_DEFAULT: true,
 | 
					 | 
				
			||||||
  DARK_MODE_ENABLED: 'general-settings:darkModeEnabled',
 | 
					 | 
				
			||||||
  DARK_MODE_ENABLED_DEFAULT: false
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -3,8 +3,9 @@ import { Observable } from 'rxjs';
 | 
				
			|||||||
import { cloneFilterRules, FilterRule } from '../data/filter-rule';
 | 
					import { cloneFilterRules, FilterRule } from '../data/filter-rule';
 | 
				
			||||||
import { PaperlessDocument } from '../data/paperless-document';
 | 
					import { PaperlessDocument } from '../data/paperless-document';
 | 
				
			||||||
import { PaperlessSavedView } from '../data/paperless-saved-view';
 | 
					import { PaperlessSavedView } from '../data/paperless-saved-view';
 | 
				
			||||||
import { DOCUMENT_LIST_SERVICE, GENERAL_SETTINGS } from '../data/storage-keys';
 | 
					import { DOCUMENT_LIST_SERVICE } from '../data/storage-keys';
 | 
				
			||||||
import { DocumentService } from './rest/document.service';
 | 
					import { DocumentService } from './rest/document.service';
 | 
				
			||||||
 | 
					import { SettingsService, SETTINGS_KEYS } from './settings.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
@ -23,7 +24,7 @@ export class DocumentListViewService {
 | 
				
			|||||||
  isReloading: boolean = false
 | 
					  isReloading: boolean = false
 | 
				
			||||||
  documents: PaperlessDocument[] = []
 | 
					  documents: PaperlessDocument[] = []
 | 
				
			||||||
  currentPage = 1
 | 
					  currentPage = 1
 | 
				
			||||||
  currentPageSize: number = +localStorage.getItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE) || GENERAL_SETTINGS.DOCUMENT_LIST_SIZE_DEFAULT
 | 
					  currentPageSize: number = this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)
 | 
				
			||||||
  collectionSize: number
 | 
					  collectionSize: number
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
@ -190,7 +191,7 @@ export class DocumentListViewService {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  updatePageSize() {
 | 
					  updatePageSize() {
 | 
				
			||||||
    let newPageSize = +localStorage.getItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE) || GENERAL_SETTINGS.DOCUMENT_LIST_SIZE_DEFAULT
 | 
					    let newPageSize = this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)
 | 
				
			||||||
    if (newPageSize != this.currentPageSize) {
 | 
					    if (newPageSize != this.currentPageSize) {
 | 
				
			||||||
      this.currentPageSize = newPageSize
 | 
					      this.currentPageSize = newPageSize
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -202,7 +203,7 @@ export class DocumentListViewService {
 | 
				
			|||||||
    this.selected.clear()
 | 
					    this.selected.clear()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private reduceSelectionToFilter() {
 | 
					  reduceSelectionToFilter() {
 | 
				
			||||||
    if (this.selected.size > 0) {
 | 
					    if (this.selected.size > 0) {
 | 
				
			||||||
      this.documentService.listAllFilteredIds(this.filterRules).subscribe(ids => {
 | 
					      this.documentService.listAllFilteredIds(this.filterRules).subscribe(ids => {
 | 
				
			||||||
        let subset = new Set<number>()
 | 
					        let subset = new Set<number>()
 | 
				
			||||||
@ -239,7 +240,7 @@ export class DocumentListViewService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(private documentService: DocumentService) {
 | 
					  constructor(private documentService: DocumentService, private settings: SettingsService) {
 | 
				
			||||||
    let documentListViewConfigJson = sessionStorage.getItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG)
 | 
					    let documentListViewConfigJson = sessionStorage.getItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG)
 | 
				
			||||||
    if (documentListViewConfigJson) {
 | 
					    if (documentListViewConfigJson) {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										16
									
								
								src-ui/src/app/services/settings.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src-ui/src/app/services/settings.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					import { TestBed } from '@angular/core/testing';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { SettingsService } from './settings.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('SettingsService', () => {
 | 
				
			||||||
 | 
					  let service: SettingsService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(() => {
 | 
				
			||||||
 | 
					    TestBed.configureTestingModule({});
 | 
				
			||||||
 | 
					    service = TestBed.inject(SettingsService);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should be created', () => {
 | 
				
			||||||
 | 
					    expect(service).toBeTruthy();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										60
									
								
								src-ui/src/app/services/settings.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src-ui/src/app/services/settings.service.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,60 @@
 | 
				
			|||||||
 | 
					import { Injectable } from '@angular/core';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface PaperlessSettings {
 | 
				
			||||||
 | 
					  key: string
 | 
				
			||||||
 | 
					  type: string
 | 
				
			||||||
 | 
					  default: any
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SETTINGS_KEYS = {
 | 
				
			||||||
 | 
					  BULK_EDIT_CONFIRMATION_DIALOGS: 'general-settings:bulk-edit:confirmation-dialogs',
 | 
				
			||||||
 | 
					  BULK_EDIT_APPLY_ON_CLOSE: 'general-settings:bulk-edit:apply-on-close',
 | 
				
			||||||
 | 
					  DOCUMENT_LIST_SIZE: 'general-settings:documentListSize',
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SETTINGS: PaperlessSettings[] = [
 | 
				
			||||||
 | 
					  {key: SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS, type: "boolean", default: true},
 | 
				
			||||||
 | 
					  {key: SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE, type: "boolean", default: false},
 | 
				
			||||||
 | 
					  {key: SETTINGS_KEYS.DOCUMENT_LIST_SIZE, type: "number", default: 50}
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable({
 | 
				
			||||||
 | 
					  providedIn: 'root'
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class SettingsService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor() { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get(key: string): any {
 | 
				
			||||||
 | 
					    let setting = SETTINGS.find(s => s.key == key)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!setting) {
 | 
				
			||||||
 | 
					      return null
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let value = localStorage.getItem(key)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (value != null) {
 | 
				
			||||||
 | 
					      switch (setting.type) {
 | 
				
			||||||
 | 
					        case "boolean":
 | 
				
			||||||
 | 
					          return JSON.parse(value)
 | 
				
			||||||
 | 
					        case "number":
 | 
				
			||||||
 | 
					          return +value
 | 
				
			||||||
 | 
					        case "string":
 | 
				
			||||||
 | 
					          return value
 | 
				
			||||||
 | 
					        default:
 | 
				
			||||||
 | 
					          return value
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return setting.default
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  set(key: string, value: any) {
 | 
				
			||||||
 | 
					    localStorage.setItem(key, value.toString())
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  unset(key: string) {
 | 
				
			||||||
 | 
					    localStorage.removeItem(key)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -2,5 +2,5 @@ export const environment = {
 | 
				
			|||||||
  production: true,
 | 
					  production: true,
 | 
				
			||||||
  apiBaseUrl: "/api/",
 | 
					  apiBaseUrl: "/api/",
 | 
				
			||||||
  appTitle: "Paperless-ng",
 | 
					  appTitle: "Paperless-ng",
 | 
				
			||||||
  version: "0.9.9"
 | 
					  version: "0.9.10"
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										
											BIN
										
									
								
								src/documents/tests/samples/test_with_bom.pdf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/documents/tests/samples/test_with_bom.pdf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							@ -847,6 +847,21 @@ class TestBulkEdit(DirectoriesMixin, APITestCase):
 | 
				
			|||||||
        self.assertEqual(args[0], [self.doc1.id])
 | 
					        self.assertEqual(args[0], [self.doc1.id])
 | 
				
			||||||
        self.assertEqual(kwargs['tag'], self.t1.id)
 | 
					        self.assertEqual(kwargs['tag'], self.t1.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @mock.patch("documents.serialisers.bulk_edit.modify_tags")
 | 
				
			||||||
 | 
					    def test_api_modify_tags(self, m):
 | 
				
			||||||
 | 
					        m.return_value = "OK"
 | 
				
			||||||
 | 
					        response = self.client.post("/api/documents/bulk_edit/", json.dumps({
 | 
				
			||||||
 | 
					            "documents": [self.doc1.id, self.doc3.id],
 | 
				
			||||||
 | 
					            "method": "modify_tags",
 | 
				
			||||||
 | 
					            "parameters": {"add_tags": [self.t1.id], "remove_tags": [self.t2.id]}
 | 
				
			||||||
 | 
					        }), content_type='application/json')
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
 | 
					        m.assert_called_once()
 | 
				
			||||||
 | 
					        args, kwargs = m.call_args
 | 
				
			||||||
 | 
					        self.assertListEqual(args[0], [self.doc1.id, self.doc3.id])
 | 
				
			||||||
 | 
					        self.assertEqual(kwargs['add_tags'], [self.t1.id])
 | 
				
			||||||
 | 
					        self.assertEqual(kwargs['remove_tags'], [self.t2.id])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @mock.patch("documents.serialisers.bulk_edit.delete")
 | 
					    @mock.patch("documents.serialisers.bulk_edit.delete")
 | 
				
			||||||
    def test_api_delete(self, m):
 | 
					    def test_api_delete(self, m):
 | 
				
			||||||
        m.return_value = "OK"
 | 
					        m.return_value = "OK"
 | 
				
			||||||
@ -927,6 +942,38 @@ class TestBulkEdit(DirectoriesMixin, APITestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        self.assertEqual(list(self.doc2.tags.all()), [self.t1])
 | 
					        self.assertEqual(list(self.doc2.tags.all()), [self.t1])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_api_modify_invalid_tags(self):
 | 
				
			||||||
 | 
					        self.assertEqual(list(self.doc2.tags.all()), [self.t1])
 | 
				
			||||||
 | 
					        response = self.client.post("/api/documents/bulk_edit/", json.dumps({
 | 
				
			||||||
 | 
					            "documents": [self.doc2.id],
 | 
				
			||||||
 | 
					            "method": "modify_tags",
 | 
				
			||||||
 | 
					            "parameters": {'add_tags': [self.t2.id, 1657], "remove_tags": [1123123]}
 | 
				
			||||||
 | 
					        }), content_type='application/json')
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 400)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_api_selection_data_empty(self):
 | 
				
			||||||
 | 
					        response = self.client.post("/api/documents/selection_data/", json.dumps({
 | 
				
			||||||
 | 
					            "documents": []
 | 
				
			||||||
 | 
					        }), content_type='application/json')
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
 | 
					        for field, Entity in [('selected_correspondents', Correspondent), ('selected_tags', Tag), ('selected_document_types', DocumentType)]:
 | 
				
			||||||
 | 
					            self.assertEqual(len(response.data[field]), Entity.objects.count())
 | 
				
			||||||
 | 
					            for correspondent in response.data[field]:
 | 
				
			||||||
 | 
					                self.assertEqual(correspondent['document_count'], 0)
 | 
				
			||||||
 | 
					            self.assertCountEqual(
 | 
				
			||||||
 | 
					                map(lambda c: c['id'], response.data[field]),
 | 
				
			||||||
 | 
					                map(lambda c: c['id'], Entity.objects.values('id')))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_api_selection_data(self):
 | 
				
			||||||
 | 
					        response = self.client.post("/api/documents/selection_data/", json.dumps({
 | 
				
			||||||
 | 
					            "documents": [self.doc1.id, self.doc2.id, self.doc4.id, self.doc5.id]
 | 
				
			||||||
 | 
					        }), content_type='application/json')
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertCountEqual(response.data['selected_correspondents'], [{"id": self.c1.id, "document_count": 1}, {"id": self.c2.id, "document_count": 0}])
 | 
				
			||||||
 | 
					        self.assertCountEqual(response.data['selected_tags'], [{"id": self.t1.id, "document_count": 2}, {"id": self.t2.id, "document_count": 1}])
 | 
				
			||||||
 | 
					        self.assertCountEqual(response.data['selected_document_types'], [{"id": self.c1.id, "document_count": 1}, {"id": self.c2.id, "document_count": 0}])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestApiAuth(APITestCase):
 | 
					class TestApiAuth(APITestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -951,3 +998,4 @@ class TestApiAuth(APITestCase):
 | 
				
			|||||||
        self.assertEqual(self.client.get("/api/search/").status_code, 401)
 | 
					        self.assertEqual(self.client.get("/api/search/").status_code, 401)
 | 
				
			||||||
        self.assertEqual(self.client.get("/api/search/auto_complete/").status_code, 401)
 | 
					        self.assertEqual(self.client.get("/api/search/auto_complete/").status_code, 401)
 | 
				
			||||||
        self.assertEqual(self.client.get("/api/documents/bulk_edit/").status_code, 401)
 | 
					        self.assertEqual(self.client.get("/api/documents/bulk_edit/").status_code, 401)
 | 
				
			||||||
 | 
					        self.assertEqual(self.client.get("/api/documents/selection_data/").status_code, 401)
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,12 @@
 | 
				
			|||||||
from datetime import datetime
 | 
					from datetime import datetime
 | 
				
			||||||
 | 
					from unittest import mock
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.test import TestCase
 | 
					from django.test import TestCase
 | 
				
			||||||
from django.utils import timezone
 | 
					from django.utils import timezone
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from documents import tasks
 | 
					from documents import tasks
 | 
				
			||||||
from documents.models import Document
 | 
					from documents.models import Document
 | 
				
			||||||
 | 
					from documents.sanity_checker import SanityError, SanityFailedError
 | 
				
			||||||
from documents.tests.utils import DirectoriesMixin
 | 
					from documents.tests.utils import DirectoriesMixin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -22,3 +24,19 @@ class TestTasks(DirectoriesMixin, TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def test_train_classifier(self):
 | 
					    def test_train_classifier(self):
 | 
				
			||||||
        tasks.train_classifier()
 | 
					        tasks.train_classifier()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @mock.patch("documents.tasks.sanity_checker.check_sanity")
 | 
				
			||||||
 | 
					    def test_sanity_check(self, m):
 | 
				
			||||||
 | 
					        m.return_value = []
 | 
				
			||||||
 | 
					        tasks.sanity_check()
 | 
				
			||||||
 | 
					        m.assert_called_once()
 | 
				
			||||||
 | 
					        m.reset_mock()
 | 
				
			||||||
 | 
					        m.return_value = [SanityError("")]
 | 
				
			||||||
 | 
					        self.assertRaises(SanityFailedError, tasks.sanity_check)
 | 
				
			||||||
 | 
					        m.assert_called_once()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_culk_update_documents(self):
 | 
				
			||||||
 | 
					        doc1 = Document.objects.create(title="test", content="my document", checksum="wow", added=timezone.now(),
 | 
				
			||||||
 | 
					                                created=timezone.now(), modified=timezone.now())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        tasks.bulk_update_documents([doc1.pk])
 | 
				
			||||||
 | 
				
			|||||||
@ -69,6 +69,8 @@ SCRATCH_DIR = os.getenv("PAPERLESS_SCRATCH_DIR", "/tmp/paperless")
 | 
				
			|||||||
# Application Definition                                                      #
 | 
					# Application Definition                                                      #
 | 
				
			||||||
###############################################################################
 | 
					###############################################################################
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					env_apps = os.getenv("PAPERLESS_APPS").split(",") if os.getenv("PAPERLESS_APPS") else []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
INSTALLED_APPS = [
 | 
					INSTALLED_APPS = [
 | 
				
			||||||
    "whitenoise.runserver_nostatic",
 | 
					    "whitenoise.runserver_nostatic",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -95,7 +97,7 @@ INSTALLED_APPS = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    "django_q",
 | 
					    "django_q",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
]
 | 
					] + env_apps
 | 
				
			||||||
 | 
					
 | 
				
			||||||
REST_FRAMEWORK = {
 | 
					REST_FRAMEWORK = {
 | 
				
			||||||
    'DEFAULT_AUTHENTICATION_CLASSES': [
 | 
					    'DEFAULT_AUTHENTICATION_CLASSES': [
 | 
				
			||||||
@ -420,3 +422,5 @@ for t in json.loads(os.getenv("PAPERLESS_FILENAME_PARSE_TRANSFORMS", "[]")):
 | 
				
			|||||||
# TODO: this should not have a prefix.
 | 
					# TODO: this should not have a prefix.
 | 
				
			||||||
# Specify the filename format for out files
 | 
					# Specify the filename format for out files
 | 
				
			||||||
PAPERLESS_FILENAME_FORMAT = os.getenv("PAPERLESS_FILENAME_FORMAT")
 | 
					PAPERLESS_FILENAME_FORMAT = os.getenv("PAPERLESS_FILENAME_FORMAT")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					THUMBNAIL_FONT_NAME = os.getenv("PAPERLESS_THUMBNAIL_FONT_NAME", "/usr/share/fonts/liberation/LiberationSerif-Regular.ttf")
 | 
				
			||||||
 | 
				
			|||||||
@ -1 +1 @@
 | 
				
			|||||||
__version__ = (0, 9, 9)
 | 
					__version__ = (0, 9, 10)
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,9 @@
 | 
				
			|||||||
import os
 | 
					import os
 | 
				
			||||||
import subprocess
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
from PIL import ImageDraw, ImageFont, Image
 | 
					from PIL import ImageDraw, ImageFont, Image
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from documents.parsers import DocumentParser, ParseError
 | 
					from documents.parsers import DocumentParser
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TextDocumentParser(DocumentParser):
 | 
					class TextDocumentParser(DocumentParser):
 | 
				
			||||||
@ -23,7 +22,8 @@ class TextDocumentParser(DocumentParser):
 | 
				
			|||||||
        img = Image.new("RGB", (500, 700), color="white")
 | 
					        img = Image.new("RGB", (500, 700), color="white")
 | 
				
			||||||
        draw = ImageDraw.Draw(img)
 | 
					        draw = ImageDraw.Draw(img)
 | 
				
			||||||
        font = ImageFont.truetype(
 | 
					        font = ImageFont.truetype(
 | 
				
			||||||
            "/usr/share/fonts/liberation/LiberationSerif-Regular.ttf", 20,
 | 
					            font=settings.THUMBNAIL_FONT_NAME,
 | 
				
			||||||
 | 
					            size=20,
 | 
				
			||||||
            layout_engine=ImageFont.LAYOUT_BASIC)
 | 
					            layout_engine=ImageFont.LAYOUT_BASIC)
 | 
				
			||||||
        draw.text((5, 5), read_text(), font=font, fill="black")
 | 
					        draw.text((5, 5), read_text(), font=font, fill="black")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user