Merge branch 'dev' into feature-created-date

This commit is contained in:
shamoon
2022-05-26 14:28:33 -07:00
committed by GitHub
118 changed files with 5276 additions and 3044 deletions

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'
import { ActivatedRoute, Params, Router } from '@angular/router'
import { ParamMap, Router } from '@angular/router'
import { Observable } from 'rxjs'
import {
cloneFilterRules,
@@ -8,14 +8,16 @@ import {
} from '../data/filter-rule'
import { PaperlessDocument } from '../data/paperless-document'
import { PaperlessSavedView } from '../data/paperless-saved-view'
import { SETTINGS_KEYS } from '../data/paperless-uisettings'
import { DOCUMENT_LIST_SERVICE } from '../data/storage-keys'
import { generateParams, parseParams } from '../utils/query-params'
import { DocumentService, DOCUMENT_SORT_FIELDS } from './rest/document.service'
import { SettingsService, SETTINGS_KEYS } from './settings.service'
import { SettingsService } from './settings.service'
/**
* Captures the current state of the list view.
*/
interface ListViewState {
export interface ListViewState {
/**
* Title of the document list view. Either "Documents" (localized) or the name of a saved view.
*/
@@ -31,7 +33,7 @@ interface ListViewState {
/**
* Total amount of documents with the current filter rules. Used to calculate the number of pages.
*/
collectionSize: number
collectionSize?: number
/**
* Currently selected sort field.
@@ -84,6 +86,32 @@ export class DocumentListViewService {
return this.activeListViewState.title
}
constructor(
private documentService: DocumentService,
private settings: SettingsService,
private router: Router
) {
let documentListViewConfigJson = localStorage.getItem(
DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG
)
if (documentListViewConfigJson) {
try {
let savedState: ListViewState = JSON.parse(documentListViewConfigJson)
// Remove null elements from the restored state
Object.keys(savedState).forEach((k) => {
if (savedState[k] == null) {
delete savedState[k]
}
})
//only use restored state attributes instead of defaults if they are not null
let newState = Object.assign(this.defaultListViewState(), savedState)
this.listViewStates.set(null, newState)
} catch (e) {
localStorage.removeItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG)
}
}
}
private defaultListViewState(): ListViewState {
return {
title: null,
@@ -121,20 +149,40 @@ export class DocumentListViewService {
if (closeCurrentView) {
this._activeSavedViewId = null
}
this.activeListViewState.filterRules = cloneFilterRules(view.filter_rules)
this.activeListViewState.sortField = view.sort_field
this.activeListViewState.sortReverse = view.sort_reverse
if (this._activeSavedViewId) {
this.activeListViewState.title = view.name
}
this.reduceSelectionToFilter()
if (!this.router.routerState.snapshot.url.includes('/view/')) {
this.router.navigate([], {
queryParams: { view: view.id },
})
}
}
reload(onFinish?) {
loadFromQueryParams(queryParams: ParamMap) {
const paramsEmpty: boolean = queryParams.keys.length == 0
let newState: ListViewState = this.listViewStates.get(null)
if (!paramsEmpty) newState = parseParams(queryParams)
if (newState == undefined) newState = this.defaultListViewState() // if nothing in local storage
this.activeListViewState.filterRules = newState.filterRules
this.activeListViewState.sortField = newState.sortField
this.activeListViewState.sortReverse = newState.sortReverse
this.activeListViewState.currentPage = newState.currentPage
this.reload(null, paramsEmpty) // update the params if there arent any
}
reload(onFinish?, updateQueryParams: boolean = true) {
this.isReloading = true
this.error = null
let activeListViewState = this.activeListViewState
this.documentService
.listFiltered(
activeListViewState.currentPage,
@@ -148,6 +196,14 @@ export class DocumentListViewService {
this.isReloading = false
activeListViewState.collectionSize = result.count
activeListViewState.documents = result.results
if (updateQueryParams && !this._activeSavedViewId) {
let base = ['/documents']
this.router.navigate(base, {
queryParams: generateParams(activeListViewState),
})
}
if (onFinish) {
onFinish()
}
@@ -190,6 +246,7 @@ export class DocumentListViewService {
) {
this.activeListViewState.sortField = 'created'
}
this._activeSavedViewId = null
this.activeListViewState.filterRules = filterRules
this.reload()
this.reduceSelectionToFilter()
@@ -201,6 +258,7 @@ export class DocumentListViewService {
}
set sortField(field: string) {
this._activeSavedViewId = null
this.activeListViewState.sortField = field
this.reload()
this.saveDocumentListView()
@@ -211,6 +269,7 @@ export class DocumentListViewService {
}
set sortReverse(reverse: boolean) {
this._activeSavedViewId = null
this.activeListViewState.sortReverse = reverse
this.reload()
this.saveDocumentListView()
@@ -220,13 +279,6 @@ export class DocumentListViewService {
return this.activeListViewState.sortReverse
}
get sortParams(): Params {
return {
sortField: this.sortField,
sortReverse: this.sortReverse,
}
}
get collectionSize(): number {
return this.activeListViewState.collectionSize
}
@@ -236,6 +288,8 @@ export class DocumentListViewService {
}
set currentPage(page: number) {
if (this.activeListViewState.currentPage == page) return
this._activeSavedViewId = null
this.activeListViewState.currentPage = page
this.reload()
this.saveDocumentListView()
@@ -272,6 +326,10 @@ export class DocumentListViewService {
}
}
quickFilter(filterRules: FilterRule[]) {
this.filterRules = filterRules
}
getLastPage(): number {
return Math.ceil(this.collectionSize / this.currentPageSize)
}
@@ -430,29 +488,4 @@ export class DocumentListViewService {
documentIndexInCurrentView(documentID: number): number {
return this.documents.map((d) => d.id).indexOf(documentID)
}
constructor(
private documentService: DocumentService,
private settings: SettingsService
) {
let documentListViewConfigJson = localStorage.getItem(
DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG
)
if (documentListViewConfigJson) {
try {
let savedState: ListViewState = JSON.parse(documentListViewConfigJson)
// Remove null elements from the restored state
Object.keys(savedState).forEach((k) => {
if (savedState[k] == null) {
delete savedState[k]
}
})
//only use restored state attributes instead of defaults if they are not null
let newState = Object.assign(this.defaultListViewState(), savedState)
this.listViewStates.set(null, newState)
} catch (e) {
localStorage.removeItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG)
}
}
}
}

View File

@@ -1,163 +0,0 @@
import { Injectable } from '@angular/core'
import { ParamMap, Params, Router } from '@angular/router'
import { FilterRule } from '../data/filter-rule'
import { FilterRuleType, FILTER_RULE_TYPES } from '../data/filter-rule-type'
import { PaperlessSavedView } from '../data/paperless-saved-view'
import { DocumentListViewService } from './document-list-view.service'
const SORT_FIELD_PARAMETER = 'sort'
const SORT_REVERSE_PARAMETER = 'reverse'
@Injectable({
providedIn: 'root',
})
export class QueryParamsService {
constructor(private router: Router, private list: DocumentListViewService) {}
private filterParams: Params = {}
private sortParams: Params = {}
updateFilterRules(
filterRules: FilterRule[],
updateQueryParams: boolean = true
) {
this.filterParams = filterRulesToQueryParams(filterRules)
if (updateQueryParams) this.updateQueryParams()
}
set sortField(field: string) {
this.sortParams[SORT_FIELD_PARAMETER] = field
this.updateQueryParams()
}
set sortReverse(reverse: boolean) {
if (!reverse) this.sortParams[SORT_REVERSE_PARAMETER] = undefined
else this.sortParams[SORT_REVERSE_PARAMETER] = reverse
this.updateQueryParams()
}
get params(): Params {
return {
...this.sortParams,
...this.filterParams,
}
}
private updateQueryParams() {
// if we were on a saved view we navigate 'away' to /documents
let base = []
if (this.router.routerState.snapshot.url.includes('/view/'))
base = ['/documents']
this.router.navigate(base, {
queryParams: this.params,
})
}
public parseQueryParams(queryParams: ParamMap) {
let filterRules = filterRulesFromQueryParams(queryParams)
if (
filterRules.length ||
queryParams.has(SORT_FIELD_PARAMETER) ||
queryParams.has(SORT_REVERSE_PARAMETER)
) {
this.list.filterRules = filterRules
this.list.sortField = queryParams.get(SORT_FIELD_PARAMETER)
this.list.sortReverse =
queryParams.has(SORT_REVERSE_PARAMETER) ||
(!queryParams.has(SORT_FIELD_PARAMETER) &&
!queryParams.has(SORT_REVERSE_PARAMETER))
this.list.reload()
} else if (
filterRules.length == 0 &&
!queryParams.has(SORT_FIELD_PARAMETER)
) {
// this is navigating to /documents so we need to update the params from the list
this.updateFilterRules(this.list.filterRules, false)
this.sortParams[SORT_FIELD_PARAMETER] = this.list.sortField
this.sortParams[SORT_REVERSE_PARAMETER] = this.list.sortReverse
this.router.navigate([], {
queryParams: this.params,
replaceUrl: true,
})
}
}
updateFromView(view: PaperlessSavedView) {
if (!this.router.routerState.snapshot.url.includes('/view/')) {
// navigation for /documents?view=
this.router.navigate([], {
queryParams: { view: view.id },
})
}
// make sure params are up-to-date
this.updateFilterRules(view.filter_rules, false)
this.sortParams[SORT_FIELD_PARAMETER] = this.list.sortField
this.sortParams[SORT_REVERSE_PARAMETER] = this.list.sortReverse
}
navigateWithFilterRules(filterRules: FilterRule[]) {
this.updateFilterRules(filterRules)
this.router.navigate(['/documents'], {
queryParams: this.params,
})
}
}
export function filterRulesToQueryParams(filterRules: FilterRule[]): Object {
if (filterRules) {
let params = {}
for (let rule of filterRules) {
let ruleType = FILTER_RULE_TYPES.find((t) => t.id == rule.rule_type)
if (ruleType.multi) {
params[ruleType.filtervar] = params[ruleType.filtervar]
? params[ruleType.filtervar] + ',' + rule.value
: rule.value
} else if (ruleType.isnull_filtervar && rule.value == null) {
params[ruleType.isnull_filtervar] = true
} else {
params[ruleType.filtervar] = rule.value
}
}
return params
} else {
return null
}
}
export function filterRulesFromQueryParams(queryParams: ParamMap) {
const allFilterRuleQueryParams: string[] = FILTER_RULE_TYPES.map(
(rt) => rt.filtervar
)
.concat(FILTER_RULE_TYPES.map((rt) => rt.isnull_filtervar))
.filter((rt) => rt !== undefined)
// transform query params to filter rules
let filterRulesFromQueryParams: FilterRule[] = []
allFilterRuleQueryParams
.filter((frqp) => queryParams.has(frqp))
.forEach((filterQueryParamName) => {
const rule_type: FilterRuleType = FILTER_RULE_TYPES.find(
(rt) =>
rt.filtervar == filterQueryParamName ||
rt.isnull_filtervar == filterQueryParamName
)
const isNullRuleType = rule_type.isnull_filtervar == filterQueryParamName
const valueURIComponent: string = queryParams.get(filterQueryParamName)
const filterQueryParamValues: string[] = rule_type.multi
? valueURIComponent.split(',')
: [valueURIComponent]
filterRulesFromQueryParams = filterRulesFromQueryParams.concat(
// map all values to filter rules
filterQueryParamValues.map((val) => {
return {
rule_type: rule_type.id,
value: isNullRuleType ? null : val,
}
})
)
})
return filterRulesFromQueryParams
}

View File

@@ -6,12 +6,13 @@ import { HttpClient, HttpParams } from '@angular/common/http'
import { Observable } from 'rxjs'
import { Results } from 'src/app/data/results'
import { FilterRule } from 'src/app/data/filter-rule'
import { map } from 'rxjs/operators'
import { map, tap } from 'rxjs/operators'
import { CorrespondentService } from './correspondent.service'
import { DocumentTypeService } from './document-type.service'
import { TagService } from './tag.service'
import { PaperlessDocumentSuggestions } from 'src/app/data/paperless-document-suggestions'
import { filterRulesToQueryParams } from '../query-params.service'
import { queryParamsFromFilterRules } from '../../utils/query-params'
import { StoragePathService } from './storage-path.service'
export const DOCUMENT_SORT_FIELDS = [
{ field: 'archive_serial_number', name: $localize`ASN` },
@@ -37,6 +38,7 @@ export interface SelectionDataItem {
}
export interface SelectionData {
selected_storage_paths: SelectionDataItem[]
selected_correspondents: SelectionDataItem[]
selected_tags: SelectionDataItem[]
selected_document_types: SelectionDataItem[]
@@ -52,7 +54,8 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument>
http: HttpClient,
private correspondentService: CorrespondentService,
private documentTypeService: DocumentTypeService,
private tagService: TagService
private tagService: TagService,
private storagePathService: StoragePathService
) {
super(http, 'documents')
}
@@ -67,7 +70,16 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument>
doc.document_type$ = this.documentTypeService.getCached(doc.document_type)
}
if (doc.tags) {
doc.tags$ = this.tagService.getCachedMany(doc.tags)
doc.tags$ = this.tagService
.getCachedMany(doc.tags)
.pipe(
tap((tags) =>
tags.sort((tagA, tagB) => tagA.name.localeCompare(tagB.name))
)
)
}
if (doc.storage_path) {
doc.storage_path$ = this.storagePathService.getCached(doc.storage_path)
}
return doc
}
@@ -85,7 +97,7 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument>
pageSize,
sortField,
sortReverse,
Object.assign(extraParams, filterRulesToQueryParams(filterRules))
Object.assign(extraParams, queryParamsFromFilterRules(filterRules))
).pipe(
map((results) => {
results.results.forEach((doc) => this.addObservablesToDocument(doc))

View File

@@ -0,0 +1,13 @@
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
import { AbstractNameFilterService } from './abstract-name-filter-service'
@Injectable({
providedIn: 'root',
})
export class StoragePathService extends AbstractNameFilterService<PaperlessStoragePath> {
constructor(http: HttpClient) {
super(http, 'storage_paths')
}
}

View File

@@ -1,4 +1,5 @@
import { DOCUMENT } from '@angular/common'
import { HttpClient } from '@angular/common/http'
import {
Inject,
Injectable,
@@ -9,17 +10,19 @@ import {
} from '@angular/core'
import { Meta } from '@angular/platform-browser'
import { CookieService } from 'ngx-cookie-service'
import { first, Observable, tap } from 'rxjs'
import {
BRIGHTNESS,
estimateBrightnessForColor,
hexToHsl,
} from 'src/app/utils/color'
export interface PaperlessSettings {
key: string
type: string
default: any
}
import { environment } from 'src/environments/environment'
import {
PaperlessUiSettings,
SETTINGS,
SETTINGS_KEYS,
} from '../data/paperless-uisettings'
import { ToastService } from './toast.service'
export interface LanguageOption {
code: string
@@ -32,89 +35,42 @@ export interface LanguageOption {
dateInputFormat?: string
}
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',
DARK_MODE_USE_SYSTEM: 'general-settings:dark-mode:use-system',
DARK_MODE_ENABLED: 'general-settings:dark-mode:enabled',
DARK_MODE_THUMB_INVERTED: 'general-settings:dark-mode:thumb-inverted',
THEME_COLOR: 'general-settings:theme:color',
USE_NATIVE_PDF_VIEWER: 'general-settings:document-details:native-pdf-viewer',
DATE_LOCALE: 'general-settings:date-display:date-locale',
DATE_FORMAT: 'general-settings:date-display:date-format',
NOTIFICATIONS_CONSUMER_NEW_DOCUMENT:
'general-settings:notifications:consumer-new-documents',
NOTIFICATIONS_CONSUMER_SUCCESS:
'general-settings:notifications:consumer-success',
NOTIFICATIONS_CONSUMER_FAILED:
'general-settings:notifications:consumer-failed',
NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD:
'general-settings:notifications:consumer-suppress-on-dashboard',
}
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 },
{ key: SETTINGS_KEYS.DARK_MODE_USE_SYSTEM, type: 'boolean', default: true },
{ key: SETTINGS_KEYS.DARK_MODE_ENABLED, type: 'boolean', default: false },
{
key: SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED,
type: 'boolean',
default: true,
},
{ key: SETTINGS_KEYS.THEME_COLOR, type: 'string', default: '' },
{ key: SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, type: 'boolean', default: false },
{ key: SETTINGS_KEYS.DATE_LOCALE, type: 'string', default: '' },
{ key: SETTINGS_KEYS.DATE_FORMAT, type: 'string', default: 'mediumDate' },
{
key: SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT,
type: 'boolean',
default: true,
},
{
key: SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS,
type: 'boolean',
default: true,
},
{
key: SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_FAILED,
type: 'boolean',
default: true,
},
{
key: SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD,
type: 'boolean',
default: true,
},
]
@Injectable({
providedIn: 'root',
})
export class SettingsService {
private renderer: Renderer2
protected baseUrl: string = environment.apiBaseUrl + 'ui_settings/'
private settings: Object = {}
public displayName: string
constructor(
private rendererFactory: RendererFactory2,
rendererFactory: RendererFactory2,
@Inject(DOCUMENT) private document,
private cookieService: CookieService,
private meta: Meta,
@Inject(LOCALE_ID) private localeId: string
@Inject(LOCALE_ID) private localeId: string,
protected http: HttpClient,
private toastService: ToastService
) {
this.renderer = rendererFactory.createRenderer(null, null)
}
this.updateAppearanceSettings()
// this is called by the app initializer in app.module
public initializeSettings(): Observable<PaperlessUiSettings> {
return this.http.get<PaperlessUiSettings>(this.baseUrl).pipe(
first(),
tap((uisettings) => {
Object.assign(this.settings, uisettings.settings)
this.maybeMigrateSettings()
// to update lang cookie
if (this.settings['language']?.length)
this.setLanguage(this.settings['language'])
this.displayName = uisettings.display_name.trim()
})
)
}
public updateAppearanceSettings(
@@ -333,11 +289,13 @@ export class SettingsService {
}
getLanguage(): string {
return this.cookieService.get(this.getLanguageCookieName())
return this.get(SETTINGS_KEYS.LANGUAGE)
}
setLanguage(language: string) {
if (language) {
this.set(SETTINGS_KEYS.LANGUAGE, language)
if (language?.length) {
// for Django
this.cookieService.set(this.getLanguageCookieName(), language)
} else {
this.cookieService.delete(this.getLanguageCookieName())
@@ -362,7 +320,16 @@ export class SettingsService {
return null
}
let value = localStorage.getItem(key)
let value = null
// parse key:key:key into nested object
const keys = key.replace('general-settings:', '').split(':')
let settingObj = this.settings
keys.forEach((keyPart, index) => {
keyPart = keyPart.replace(/-/g, '_')
if (!settingObj.hasOwnProperty(keyPart)) return
if (index == keys.length - 1) value = settingObj[keyPart]
else settingObj = settingObj[keyPart]
})
if (value != null) {
switch (setting.type) {
@@ -381,10 +348,57 @@ export class SettingsService {
}
set(key: string, value: any) {
localStorage.setItem(key, value.toString())
// parse key:key:key into nested object
let settingObj = this.settings
const keys = key.replace('general-settings:', '').split(':')
keys.forEach((keyPart, index) => {
keyPart = keyPart.replace(/-/g, '_')
if (!settingObj.hasOwnProperty(keyPart)) settingObj[keyPart] = {}
if (index == keys.length - 1) settingObj[keyPart] = value
else settingObj = settingObj[keyPart]
})
}
unset(key: string) {
localStorage.removeItem(key)
storeSettings(): Observable<any> {
return this.http.post(this.baseUrl, { settings: this.settings })
}
maybeMigrateSettings() {
if (
!this.settings.hasOwnProperty('documentListSize') &&
localStorage.getItem(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)
) {
// lets migrate
const successMessage = $localize`Successfully completed one-time migratration of settings to the database!`
const errorMessage = $localize`Unable to migrate settings to the database, please try saving manually.`
try {
for (const setting in SETTINGS_KEYS) {
const key = SETTINGS_KEYS[setting]
const value = localStorage.getItem(key)
this.set(key, value)
}
this.set(
SETTINGS_KEYS.LANGUAGE,
this.cookieService.get(this.getLanguageCookieName())
)
} catch (error) {
this.toastService.showError(errorMessage)
console.log(error)
}
this.storeSettings()
.pipe(first())
.subscribe({
next: () => {
this.updateAppearanceSettings()
this.toastService.showInfo(successMessage)
},
error: (e) => {
this.toastService.showError(errorMessage)
console.log(e)
},
})
}
}
}