mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-30 18:27:45 -05:00
Feature: customizable fields display for documents, saved views & dashboard widgets (#6439)
This commit is contained in:
@@ -18,6 +18,8 @@ describe('ConsumerStatusService', () => {
|
||||
let httpTestingController: HttpTestingController
|
||||
let consumerStatusService: ConsumerStatusService
|
||||
let documentService: DocumentService
|
||||
let settingsService: SettingsService
|
||||
|
||||
const server = new WS(
|
||||
`${environment.webSocketProtocol}//${environment.webSocketHost}${environment.webSocketBaseUrl}status/`,
|
||||
{ jsonProtocol: true }
|
||||
@@ -25,25 +27,17 @@ describe('ConsumerStatusService', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
ConsumerStatusService,
|
||||
DocumentService,
|
||||
SettingsService,
|
||||
{
|
||||
provide: SettingsService,
|
||||
useValue: {
|
||||
currentUser: {
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
is_superuser: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
providers: [ConsumerStatusService, DocumentService, SettingsService],
|
||||
imports: [HttpClientTestingModule],
|
||||
})
|
||||
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
settingsService.currentUser = {
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
is_superuser: false,
|
||||
}
|
||||
consumerStatusService = TestBed.inject(ConsumerStatusService)
|
||||
documentService = TestBed.inject(DocumentService)
|
||||
})
|
||||
|
@@ -19,6 +19,11 @@ import { routes } from 'src/app/app-routing.module'
|
||||
import { PermissionsGuard } from '../guards/permissions.guard'
|
||||
import { SettingsService } from './settings.service'
|
||||
import { SETTINGS_KEYS } from '../data/ui-settings'
|
||||
import {
|
||||
DisplayMode,
|
||||
DisplayField,
|
||||
DEFAULT_DISPLAY_FIELDS,
|
||||
} from '../data/document'
|
||||
|
||||
const documents = [
|
||||
{
|
||||
@@ -213,7 +218,7 @@ describe('DocumentListViewService', () => {
|
||||
documentListViewService.loadFromQueryParams(convertToParamMap(params))
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/?page=${page}&page_size=${
|
||||
documentListViewService.currentPageSize
|
||||
documentListViewService.pageSize
|
||||
}&ordering=${reverse ? '-' : ''}${sort}&truncate_content=true`
|
||||
)
|
||||
expect(req.request.method).toEqual('GET')
|
||||
@@ -231,7 +236,7 @@ describe('DocumentListViewService', () => {
|
||||
}
|
||||
documentListViewService.loadFromQueryParams(convertToParamMap(params))
|
||||
let req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.currentPageSize}&ordering=-added&truncate_content=true&tags__id__all=${tags__id__all}`
|
||||
`${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.pageSize}&ordering=-added&truncate_content=true&tags__id__all=${tags__id__all}`
|
||||
)
|
||||
expect(req.request.method).toEqual('GET')
|
||||
expect(documentListViewService.filterRules).toEqual([
|
||||
@@ -249,7 +254,7 @@ describe('DocumentListViewService', () => {
|
||||
it('should use filter rules to update query params', () => {
|
||||
documentListViewService.filterRules = filterRules
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.currentPageSize}&ordering=-created&truncate_content=true&tags__id__all=${tags__id__all}`
|
||||
`${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.pageSize}&ordering=-created&truncate_content=true&tags__id__all=${tags__id__all}`
|
||||
)
|
||||
expect(req.request.method).toEqual('GET')
|
||||
})
|
||||
@@ -257,7 +262,7 @@ describe('DocumentListViewService', () => {
|
||||
it('should support quick filter', () => {
|
||||
documentListViewService.quickFilter(filterRules)
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.currentPageSize}&ordering=-created&truncate_content=true&tags__id__all=${tags__id__all}`
|
||||
`${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.pageSize}&ordering=-created&truncate_content=true&tags__id__all=${tags__id__all}`
|
||||
)
|
||||
expect(req.request.method).toEqual('GET')
|
||||
})
|
||||
@@ -280,7 +285,7 @@ describe('DocumentListViewService', () => {
|
||||
convertToParamMap(params)
|
||||
)
|
||||
let req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/?page=${page}&page_size=${documentListViewService.currentPageSize}&ordering=-added&truncate_content=true&tags__id__all=${tags__id__all}`
|
||||
`${environment.apiBaseUrl}documents/?page=${page}&page_size=${documentListViewService.pageSize}&ordering=-added&truncate_content=true&tags__id__all=${tags__id__all}`
|
||||
)
|
||||
expect(req.request.method).toEqual('GET')
|
||||
// reset the list
|
||||
@@ -305,8 +310,7 @@ describe('DocumentListViewService', () => {
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||
)
|
||||
expect(documentListViewService.currentPage).toEqual(1)
|
||||
documentListViewService.currentPageSize = 3
|
||||
documentListViewService.reload()
|
||||
documentListViewService.pageSize = 3
|
||||
req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=3&ordering=-created&truncate_content=true`
|
||||
)
|
||||
@@ -362,7 +366,10 @@ describe('DocumentListViewService', () => {
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue(documents)
|
||||
expect(documentListViewService.currentPage).toEqual(1)
|
||||
documentListViewService.currentPageSize = 3
|
||||
documentListViewService.pageSize = 3
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=3&ordering=-created&truncate_content=true`
|
||||
)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'getLastPage')
|
||||
.mockReturnValue(Math.ceil(documents.length / 3))
|
||||
@@ -410,7 +417,13 @@ describe('DocumentListViewService', () => {
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue(documents)
|
||||
documentListViewService.currentPage = 2
|
||||
documentListViewService.currentPageSize = 3
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=2&page_size=50&ordering=-created&truncate_content=true`
|
||||
)
|
||||
documentListViewService.pageSize = 3
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=2&page_size=3&ordering=-created&truncate_content=true`
|
||||
)
|
||||
const reloadSpy = jest.spyOn(documentListViewService, 'reload')
|
||||
documentListViewService.getPrevious(1).subscribe({
|
||||
next: () => {},
|
||||
@@ -426,8 +439,7 @@ describe('DocumentListViewService', () => {
|
||||
|
||||
it('should update page size from settings', () => {
|
||||
settingsService.set(SETTINGS_KEYS.DOCUMENT_LIST_SIZE, 10)
|
||||
documentListViewService.updatePageSize()
|
||||
expect(documentListViewService.currentPageSize).toEqual(10)
|
||||
expect(documentListViewService.pageSize).toEqual(10)
|
||||
})
|
||||
|
||||
it('should support select a document', () => {
|
||||
@@ -459,8 +471,7 @@ describe('DocumentListViewService', () => {
|
||||
})
|
||||
|
||||
it('should support select page', () => {
|
||||
documentListViewService.currentPageSize = 3
|
||||
documentListViewService.reload()
|
||||
documentListViewService.pageSize = 3
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=3&ordering=-created&truncate_content=true`
|
||||
)
|
||||
@@ -544,4 +555,40 @@ describe('DocumentListViewService', () => {
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||
)
|
||||
})
|
||||
|
||||
it('should update default view state when display mode changes', () => {
|
||||
const localStorageSpy = jest.spyOn(localStorage, 'setItem')
|
||||
expect(documentListViewService.displayMode).toEqual(DisplayMode.SMALL_CARDS)
|
||||
documentListViewService.displayMode = DisplayMode.LARGE_CARDS
|
||||
expect(documentListViewService.displayMode).toEqual(DisplayMode.LARGE_CARDS)
|
||||
documentListViewService.displayMode = 'details' as any // legacy
|
||||
expect(documentListViewService.displayMode).toEqual(DisplayMode.TABLE)
|
||||
expect(localStorageSpy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should update default view state when display fields change', () => {
|
||||
const localStorageSpy = jest.spyOn(localStorage, 'setItem')
|
||||
documentListViewService.displayFields = [
|
||||
DisplayField.ADDED,
|
||||
DisplayField.TITLE,
|
||||
]
|
||||
expect(documentListViewService.displayFields).toEqual([
|
||||
DisplayField.ADDED,
|
||||
DisplayField.TITLE,
|
||||
])
|
||||
expect(localStorageSpy).toHaveBeenCalled()
|
||||
// reload triggered
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||
)
|
||||
documentListViewService.displayFields = null
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||
)
|
||||
expect(documentListViewService.displayFields).toEqual(
|
||||
DEFAULT_DISPLAY_FIELDS.filter((f) => f.id !== DisplayField.ADDED).map(
|
||||
(f) => f.id
|
||||
)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@@ -7,16 +7,17 @@ import {
|
||||
cloneFilterRules,
|
||||
isFullTextFilterRule,
|
||||
} from '../utils/filter-rules'
|
||||
import { Document } from '../data/document'
|
||||
import {
|
||||
DEFAULT_DISPLAY_FIELDS,
|
||||
DisplayField,
|
||||
DisplayMode,
|
||||
Document,
|
||||
} from '../data/document'
|
||||
import { SavedView } from '../data/saved-view'
|
||||
import { SETTINGS_KEYS } from '../data/ui-settings'
|
||||
import { DOCUMENT_LIST_SERVICE } from '../data/storage-keys'
|
||||
import { paramsFromViewState, paramsToViewState } from '../utils/query-params'
|
||||
import {
|
||||
DocumentService,
|
||||
DOCUMENT_SORT_FIELDS,
|
||||
SelectionData,
|
||||
} from './rest/document.service'
|
||||
import { DocumentService, SelectionData } from './rest/document.service'
|
||||
import { SettingsService } from './settings.service'
|
||||
|
||||
/**
|
||||
@@ -59,6 +60,21 @@ export interface ListViewState {
|
||||
* Contains the IDs of all selected documents.
|
||||
*/
|
||||
selected?: Set<number>
|
||||
|
||||
/**
|
||||
* The page size of the list view.
|
||||
*/
|
||||
pageSize?: number
|
||||
|
||||
/**
|
||||
* Display mode of the list view.
|
||||
*/
|
||||
displayMode?: DisplayMode
|
||||
|
||||
/**
|
||||
* The fields to display in the document list.
|
||||
*/
|
||||
displayFields?: DisplayField[]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,8 +96,6 @@ export class DocumentListViewService {
|
||||
|
||||
selectionData?: SelectionData
|
||||
|
||||
currentPageSize: number = this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)
|
||||
|
||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||
|
||||
private listViewStates: Map<number, ListViewState> = new Map()
|
||||
@@ -113,7 +127,7 @@ export class DocumentListViewService {
|
||||
delete savedState[k]
|
||||
}
|
||||
})
|
||||
//only use restored state attributes instead of defaults if they are not null
|
||||
// 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) {
|
||||
@@ -176,6 +190,9 @@ export class DocumentListViewService {
|
||||
if (this._activeSavedViewId) {
|
||||
this.activeListViewState.title = view.name
|
||||
}
|
||||
this.activeListViewState.displayMode = view.display_mode
|
||||
this.activeListViewState.pageSize = view.page_size
|
||||
this.activeListViewState.displayFields = view.display_fields
|
||||
|
||||
this.reduceSelectionToFilter()
|
||||
|
||||
@@ -220,7 +237,7 @@ export class DocumentListViewService {
|
||||
this.documentService
|
||||
.listFiltered(
|
||||
activeListViewState.currentPage,
|
||||
this.currentPageSize,
|
||||
activeListViewState.pageSize ?? this.pageSize,
|
||||
activeListViewState.sortField,
|
||||
activeListViewState.sortReverse,
|
||||
activeListViewState.filterRules,
|
||||
@@ -281,9 +298,8 @@ export class DocumentListViewService {
|
||||
errorMessage = Object.keys(error.error)
|
||||
.map((fieldName) => {
|
||||
const fieldError: Array<string> = error.error[fieldName]
|
||||
return `${DOCUMENT_SORT_FIELDS.find(
|
||||
(f) => f.field == fieldName
|
||||
)?.name}: ${fieldError[0]}`
|
||||
return `${this.sortFields.find((f) => f.field == fieldName)
|
||||
?.name}: ${fieldError[0]}`
|
||||
})
|
||||
.join(', ')
|
||||
} else {
|
||||
@@ -312,6 +328,14 @@ export class DocumentListViewService {
|
||||
return this.activeListViewState.filterRules
|
||||
}
|
||||
|
||||
get sortFields(): any[] {
|
||||
return this.documentService.sortFields
|
||||
}
|
||||
|
||||
get sortFieldsFullText(): any[] {
|
||||
return this.documentService.sortFieldsFullText
|
||||
}
|
||||
|
||||
set sortField(field: string) {
|
||||
this.activeListViewState.sortField = field
|
||||
this.reload()
|
||||
@@ -362,6 +386,51 @@ export class DocumentListViewService {
|
||||
this.saveDocumentListView()
|
||||
}
|
||||
|
||||
set displayMode(mode: DisplayMode) {
|
||||
this.activeListViewState.displayMode = mode
|
||||
this.saveDocumentListView()
|
||||
}
|
||||
|
||||
get displayMode(): DisplayMode {
|
||||
const mode = this.activeListViewState.displayMode ?? DisplayMode.SMALL_CARDS
|
||||
if (mode === ('details' as any)) {
|
||||
// legacy
|
||||
return DisplayMode.TABLE
|
||||
}
|
||||
return mode
|
||||
}
|
||||
|
||||
set pageSize(size: number) {
|
||||
this.activeListViewState.pageSize = size
|
||||
this.reload()
|
||||
this.saveDocumentListView()
|
||||
}
|
||||
|
||||
get pageSize(): number {
|
||||
return (
|
||||
this.activeListViewState.pageSize ??
|
||||
this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)
|
||||
)
|
||||
}
|
||||
|
||||
get displayFields(): DisplayField[] {
|
||||
let fields =
|
||||
this.activeListViewState.displayFields ??
|
||||
DEFAULT_DISPLAY_FIELDS.map((f) => f.id)
|
||||
if (!this.activeListViewState.displayFields) {
|
||||
fields = fields.filter((f) => f !== DisplayField.ADDED)
|
||||
}
|
||||
return fields.filter(
|
||||
(field) =>
|
||||
this.settings.allDisplayFields.find((f) => f.id === field) !== undefined
|
||||
)
|
||||
}
|
||||
|
||||
set displayFields(fields: DisplayField[]) {
|
||||
this.activeListViewState.displayFields = fields
|
||||
this.saveDocumentListView()
|
||||
}
|
||||
|
||||
private saveDocumentListView() {
|
||||
if (this._activeSavedViewId == null) {
|
||||
let savedState: ListViewState = {
|
||||
@@ -370,6 +439,8 @@ export class DocumentListViewService {
|
||||
filterRules: this.activeListViewState.filterRules,
|
||||
sortField: this.activeListViewState.sortField,
|
||||
sortReverse: this.activeListViewState.sortReverse,
|
||||
displayMode: this.activeListViewState.displayMode,
|
||||
displayFields: this.activeListViewState.displayFields,
|
||||
}
|
||||
localStorage.setItem(
|
||||
DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG,
|
||||
@@ -385,7 +456,7 @@ export class DocumentListViewService {
|
||||
}
|
||||
|
||||
getLastPage(): number {
|
||||
return Math.ceil(this.collectionSize / this.currentPageSize)
|
||||
return Math.ceil(this.collectionSize / this.pageSize)
|
||||
}
|
||||
|
||||
hasNext(doc: number) {
|
||||
@@ -452,13 +523,6 @@ export class DocumentListViewService {
|
||||
})
|
||||
}
|
||||
|
||||
updatePageSize() {
|
||||
let newPageSize = this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)
|
||||
if (newPageSize != this.currentPageSize) {
|
||||
this.currentPageSize = newPageSize
|
||||
}
|
||||
}
|
||||
|
||||
selectNone() {
|
||||
this.selected.clear()
|
||||
this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null
|
||||
|
@@ -1,9 +1,7 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { AbstractPaperlessService } from './abstract-paperless-service'
|
||||
import { Observable } from 'rxjs'
|
||||
import { CustomField } from 'src/app/data/custom-field'
|
||||
import { CustomFieldInstance } from 'src/app/data/custom-field-instance'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
|
@@ -9,11 +9,17 @@ import { DocumentService } from './document.service'
|
||||
import { FILTER_TITLE } from 'src/app/data/filter-rule-type'
|
||||
import { SettingsService } from '../settings.service'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
import {
|
||||
DOCUMENT_SORT_FIELDS,
|
||||
DOCUMENT_SORT_FIELDS_FULLTEXT,
|
||||
} from 'src/app/data/document'
|
||||
import { PermissionsService } from '../permissions.service'
|
||||
|
||||
let httpTestingController: HttpTestingController
|
||||
let service: DocumentService
|
||||
let subscription: Subscription
|
||||
let settingsService: SettingsService
|
||||
|
||||
const endpoint = 'documents'
|
||||
const documents = [
|
||||
{
|
||||
@@ -275,6 +281,31 @@ describe(`DocumentService`, () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should construct sort fields respecting permissions', () => {
|
||||
expect(
|
||||
service.sortFields.find((f) => f.field === 'correspondent__name')
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
service.sortFields.find((f) => f.field === 'document_type__name')
|
||||
).toBeUndefined()
|
||||
|
||||
const permissionsService: PermissionsService =
|
||||
TestBed.inject(PermissionsService)
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
service['setupSortFields']()
|
||||
expect(service.sortFields).toEqual(DOCUMENT_SORT_FIELDS)
|
||||
expect(service.sortFieldsFullText).toEqual([
|
||||
...DOCUMENT_SORT_FIELDS,
|
||||
...DOCUMENT_SORT_FIELDS_FULLTEXT,
|
||||
])
|
||||
|
||||
settingsService.set(SETTINGS_KEYS.NOTES_ENABLED, false)
|
||||
service['setupSortFields']()
|
||||
expect(
|
||||
service.sortFields.find((f) => f.field === 'num_notes')
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
subscription?.unsubscribe()
|
||||
httpTestingController.verify()
|
||||
|
@@ -1,5 +1,9 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Document } from 'src/app/data/document'
|
||||
import {
|
||||
DOCUMENT_SORT_FIELDS,
|
||||
DOCUMENT_SORT_FIELDS_FULLTEXT,
|
||||
Document,
|
||||
} from 'src/app/data/document'
|
||||
import { DocumentMetadata } from 'src/app/data/document-metadata'
|
||||
import { AbstractPaperlessService } from './abstract-paperless-service'
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
@@ -22,26 +26,6 @@ import { SettingsService } from '../settings.service'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
import { AuditLogEntry } from 'src/app/data/auditlog-entry'
|
||||
|
||||
export const DOCUMENT_SORT_FIELDS = [
|
||||
{ field: 'archive_serial_number', name: $localize`ASN` },
|
||||
{ field: 'correspondent__name', name: $localize`Correspondent` },
|
||||
{ field: 'title', name: $localize`Title` },
|
||||
{ field: 'document_type__name', name: $localize`Document type` },
|
||||
{ field: 'created', name: $localize`Created` },
|
||||
{ field: 'added', name: $localize`Added` },
|
||||
{ field: 'modified', name: $localize`Modified` },
|
||||
{ field: 'num_notes', name: $localize`Notes` },
|
||||
{ field: 'owner', name: $localize`Owner` },
|
||||
]
|
||||
|
||||
export const DOCUMENT_SORT_FIELDS_FULLTEXT = [
|
||||
...DOCUMENT_SORT_FIELDS,
|
||||
{
|
||||
field: 'score',
|
||||
name: $localize`:Score is a value returned by the full text search engine and specifies how well a result matches the given query:Search score`,
|
||||
},
|
||||
]
|
||||
|
||||
export interface SelectionDataItem {
|
||||
id: number
|
||||
document_count: number
|
||||
@@ -60,6 +44,16 @@ export interface SelectionData {
|
||||
export class DocumentService extends AbstractPaperlessService<Document> {
|
||||
private _searchQuery: string
|
||||
|
||||
private _sortFields
|
||||
get sortFields() {
|
||||
return this._sortFields
|
||||
}
|
||||
|
||||
private _sortFieldsFullText
|
||||
get sortFieldsFullText() {
|
||||
return this._sortFieldsFullText
|
||||
}
|
||||
|
||||
constructor(
|
||||
http: HttpClient,
|
||||
private correspondentService: CorrespondentService,
|
||||
@@ -70,6 +64,46 @@ export class DocumentService extends AbstractPaperlessService<Document> {
|
||||
private settingsService: SettingsService
|
||||
) {
|
||||
super(http, 'documents')
|
||||
this.setupSortFields()
|
||||
}
|
||||
|
||||
private setupSortFields() {
|
||||
this._sortFields = [...DOCUMENT_SORT_FIELDS]
|
||||
let excludes = []
|
||||
if (
|
||||
!this.permissionsService.currentUserCan(
|
||||
PermissionAction.View,
|
||||
PermissionType.Correspondent
|
||||
)
|
||||
) {
|
||||
excludes.push('correspondent__name')
|
||||
}
|
||||
if (
|
||||
!this.permissionsService.currentUserCan(
|
||||
PermissionAction.View,
|
||||
PermissionType.DocumentType
|
||||
)
|
||||
) {
|
||||
excludes.push('document_type__name')
|
||||
}
|
||||
if (
|
||||
!this.permissionsService.currentUserCan(
|
||||
PermissionAction.View,
|
||||
PermissionType.User
|
||||
)
|
||||
) {
|
||||
excludes.push('owner')
|
||||
}
|
||||
if (!this.settingsService.get(SETTINGS_KEYS.NOTES_ENABLED)) {
|
||||
excludes.push('num_notes')
|
||||
}
|
||||
this._sortFields = this._sortFields.filter(
|
||||
(field) => !excludes.includes(field.field)
|
||||
)
|
||||
this._sortFieldsFullText = [
|
||||
...this._sortFields,
|
||||
...DOCUMENT_SORT_FIELDS_FULLTEXT,
|
||||
]
|
||||
}
|
||||
|
||||
addObservablesToDocument(doc: Document) {
|
||||
|
@@ -7,17 +7,38 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { CookieService } from 'ngx-cookie-service'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { Subscription, of } from 'rxjs'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { AppModule } from '../app.module'
|
||||
import { UiSettings, SETTINGS_KEYS } from '../data/ui-settings'
|
||||
import { SettingsService } from './settings.service'
|
||||
import { SavedView } from '../data/saved-view'
|
||||
import { CustomFieldsService } from './rest/custom-fields.service'
|
||||
import { CustomFieldDataType } from '../data/custom-field'
|
||||
import { PermissionsService } from './permissions.service'
|
||||
import { DEFAULT_DISPLAY_FIELDS, DisplayField } from '../data/document'
|
||||
|
||||
const customFields = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Field 1',
|
||||
created: new Date(),
|
||||
data_type: CustomFieldDataType.Monetary,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Field 2',
|
||||
created: new Date(),
|
||||
data_type: CustomFieldDataType.String,
|
||||
},
|
||||
]
|
||||
|
||||
describe('SettingsService', () => {
|
||||
let httpTestingController: HttpTestingController
|
||||
let settingsService: SettingsService
|
||||
let cookieService: CookieService
|
||||
let customFieldsService: CustomFieldsService
|
||||
let permissionService: PermissionsService
|
||||
let subscription: Subscription
|
||||
|
||||
const ui_settings: UiSettings = {
|
||||
@@ -76,12 +97,14 @@ describe('SettingsService', () => {
|
||||
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
cookieService = TestBed.inject(CookieService)
|
||||
customFieldsService = TestBed.inject(CustomFieldsService)
|
||||
permissionService = TestBed.inject(PermissionsService)
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
subscription?.unsubscribe()
|
||||
httpTestingController.verify()
|
||||
// httpTestingController.verify()
|
||||
})
|
||||
|
||||
it('calls ui_settings api endpoint on initialize', () => {
|
||||
@@ -314,4 +337,51 @@ describe('SettingsService', () => {
|
||||
// post for migrate
|
||||
httpTestingController.expectOne(`${environment.apiBaseUrl}ui_settings/`)
|
||||
})
|
||||
|
||||
it('should hide fields if no perms or disabled', () => {
|
||||
jest.spyOn(permissionService, 'currentUserCan').mockReturnValue(false)
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}ui_settings/`
|
||||
)
|
||||
req.flush(ui_settings)
|
||||
settingsService.initializeDisplayFields()
|
||||
expect(
|
||||
settingsService.allDisplayFields.includes(DEFAULT_DISPLAY_FIELDS[0])
|
||||
).toBeTruthy() // title
|
||||
expect(
|
||||
settingsService.allDisplayFields.includes(DEFAULT_DISPLAY_FIELDS[4])
|
||||
).toBeFalsy() // correspondent
|
||||
|
||||
settingsService.set(SETTINGS_KEYS.NOTES_ENABLED, false)
|
||||
settingsService.initializeDisplayFields()
|
||||
expect(
|
||||
settingsService.allDisplayFields.includes(DEFAULT_DISPLAY_FIELDS[8])
|
||||
).toBeFalsy() // notes
|
||||
|
||||
jest.spyOn(permissionService, 'currentUserCan').mockReturnValue(true)
|
||||
settingsService.initializeDisplayFields()
|
||||
expect(
|
||||
settingsService.allDisplayFields.includes(DEFAULT_DISPLAY_FIELDS[4])
|
||||
).toBeTruthy() // correspondent
|
||||
})
|
||||
|
||||
it('should dynamically create display fields options including custom fields', () => {
|
||||
jest.spyOn(permissionService, 'currentUserCan').mockReturnValue(true)
|
||||
jest.spyOn(customFieldsService, 'listAll').mockReturnValue(
|
||||
of({
|
||||
all: customFields.map((f) => f.id),
|
||||
count: customFields.length,
|
||||
results: customFields.concat([]),
|
||||
})
|
||||
)
|
||||
settingsService.initializeDisplayFields()
|
||||
expect(
|
||||
settingsService.allDisplayFields.includes(DEFAULT_DISPLAY_FIELDS[0])
|
||||
).toBeTruthy()
|
||||
expect(
|
||||
settingsService.allDisplayFields.find(
|
||||
(f) => f.id === `${DisplayField.CUSTOM_FIELD}${customFields[0].id}`
|
||||
).name
|
||||
).toEqual(customFields[0].name)
|
||||
})
|
||||
})
|
||||
|
@@ -19,9 +19,15 @@ import {
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { UiSettings, SETTINGS, SETTINGS_KEYS } from '../data/ui-settings'
|
||||
import { User } from '../data/user'
|
||||
import { PermissionsService } from './permissions.service'
|
||||
import {
|
||||
PermissionAction,
|
||||
PermissionType,
|
||||
PermissionsService,
|
||||
} from './permissions.service'
|
||||
import { ToastService } from './toast.service'
|
||||
import { SavedView } from '../data/saved-view'
|
||||
import { CustomFieldsService } from './rest/custom-fields.service'
|
||||
import { DEFAULT_DISPLAY_FIELDS, DisplayField } from '../data/document'
|
||||
|
||||
export interface LanguageOption {
|
||||
code: string
|
||||
@@ -257,6 +263,12 @@ export class SettingsService {
|
||||
public globalDropzoneActive: boolean = false
|
||||
public organizingSidebarSavedViews: boolean = false
|
||||
|
||||
private _allDisplayFields: Array<{ id: DisplayField; name: string }> =
|
||||
DEFAULT_DISPLAY_FIELDS
|
||||
public get allDisplayFields(): Array<{ id: DisplayField; name: string }> {
|
||||
return this._allDisplayFields
|
||||
}
|
||||
|
||||
constructor(
|
||||
rendererFactory: RendererFactory2,
|
||||
@Inject(DOCUMENT) private document,
|
||||
@@ -265,7 +277,8 @@ export class SettingsService {
|
||||
@Inject(LOCALE_ID) private localeId: string,
|
||||
protected http: HttpClient,
|
||||
private toastService: ToastService,
|
||||
private permissionsService: PermissionsService
|
||||
private permissionsService: PermissionsService,
|
||||
private customFieldsService: CustomFieldsService
|
||||
) {
|
||||
this._renderer = rendererFactory.createRenderer(null, null)
|
||||
}
|
||||
@@ -288,10 +301,70 @@ export class SettingsService {
|
||||
uisettings.permissions,
|
||||
this.currentUser
|
||||
)
|
||||
|
||||
this.initializeDisplayFields()
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
public initializeDisplayFields() {
|
||||
this._allDisplayFields = DEFAULT_DISPLAY_FIELDS
|
||||
|
||||
this._allDisplayFields = this._allDisplayFields
|
||||
?.map((field) => {
|
||||
if (
|
||||
field.id === DisplayField.NOTES &&
|
||||
!this.get(SETTINGS_KEYS.NOTES_ENABLED)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (
|
||||
[
|
||||
DisplayField.TITLE,
|
||||
DisplayField.CREATED,
|
||||
DisplayField.ADDED,
|
||||
DisplayField.ASN,
|
||||
DisplayField.SHARED,
|
||||
].includes(field.id)
|
||||
) {
|
||||
return field
|
||||
}
|
||||
|
||||
let type: PermissionType = Object.values(PermissionType).find((t) =>
|
||||
t.includes(field.id)
|
||||
)
|
||||
if (field.id === DisplayField.OWNER) {
|
||||
type = PermissionType.User
|
||||
}
|
||||
return this.permissionsService.currentUserCan(
|
||||
PermissionAction.View,
|
||||
type
|
||||
)
|
||||
? field
|
||||
: null
|
||||
})
|
||||
.filter((f) => f)
|
||||
|
||||
if (
|
||||
this.permissionsService.currentUserCan(
|
||||
PermissionAction.View,
|
||||
PermissionType.CustomField
|
||||
)
|
||||
) {
|
||||
this.customFieldsService.listAll().subscribe((r) => {
|
||||
this._allDisplayFields = this._allDisplayFields.concat(
|
||||
r.results.map((field) => {
|
||||
return {
|
||||
id: `${DisplayField.CUSTOM_FIELD}${field.id}` as any,
|
||||
name: field.name,
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
get displayName(): string {
|
||||
return (
|
||||
this.currentUser.first_name ??
|
||||
|
Reference in New Issue
Block a user