mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-02 13:45:10 -05:00
Feature: customizable fields display for documents, saved views & dashboard widgets (#6439)
This commit is contained in:
parent
7a0334f353
commit
bd4476d484
@ -124,7 +124,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"size": -1,
|
"size": -1,
|
||||||
"mimeType": "application/json",
|
"mimeType": "application/json",
|
||||||
"text": "{\"count\":6,\"next\":null,\"previous\":null,\"all\":[8,17,7,4,11,15],\"results\":[{\"id\":8,\"name\":\"Correspondent 2\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":3,\"value\":\"2\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":17,\"name\":\"In the Last Month\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":20,\"value\":\"created:[-1 month to now]\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":7,\"name\":\"Inbox\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"9\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":4,\"name\":\"Recently Added\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[],\"owner\":\"2\",\"user_can_change\":true},{\"id\":11,\"name\":\"Tag: Another Sample Tag\",\"show_on_dashboard\":false,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"4\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":15,\"name\":\"View ASN not empty\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":18,\"value\":\"false\"}],\"owner\":\"2\",\"user_can_change\":true}]}"
|
"text": "{\"count\":6,\"next\":null,\"previous\":null,\"all\":[8,17,7,4,11,15],\"results\":[{\"id\":8,\"name\":\"Correspondent 2\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":3,\"value\":\"2\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":17,\"name\":\"In the Last Month\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":20,\"value\":\"created:[-1 month to now]\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":7,\"name\":\"Inbox\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"9\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":4,\"name\":\"Recently Added\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":11,\"name\":\"Tag: Another Sample Tag\",\"show_on_dashboard\":false,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"4\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":15,\"name\":\"View ASN not empty\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":18,\"value\":\"false\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]}]}"
|
||||||
},
|
},
|
||||||
"headersSize": -1,
|
"headersSize": -1,
|
||||||
"bodySize": -1,
|
"bodySize": -1,
|
||||||
|
@ -124,7 +124,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"size": -1,
|
"size": -1,
|
||||||
"mimeType": "application/json",
|
"mimeType": "application/json",
|
||||||
"text": "{\"count\":6,\"next\":null,\"previous\":null,\"all\":[8,17,7,4,11,15],\"results\":[{\"id\":8,\"name\":\"Correspondent 2\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":3,\"value\":\"2\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":17,\"name\":\"In the Last Month\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":20,\"value\":\"created:[-1 month to now]\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":7,\"name\":\"Inbox\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"9\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":4,\"name\":\"Recently Added\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[],\"owner\":\"2\",\"user_can_change\":true},{\"id\":11,\"name\":\"Tag: Another Sample Tag\",\"show_on_dashboard\":false,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"4\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":15,\"name\":\"View ASN not empty\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":18,\"value\":\"false\"}],\"owner\":\"2\",\"user_can_change\":true}]}"
|
"text": "{\"count\":6,\"next\":null,\"previous\":null,\"all\":[8,17,7,4,11,15],\"results\":[{\"id\":8,\"name\":\"Correspondent 2\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":3,\"value\":\"2\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":17,\"name\":\"In the Last Month\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":20,\"value\":\"created:[-1 month to now]\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":7,\"name\":\"Inbox\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"9\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":4,\"name\":\"Recently Added\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":11,\"name\":\"Tag: Another Sample Tag\",\"show_on_dashboard\":false,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"4\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":15,\"name\":\"View ASN not empty\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":18,\"value\":\"false\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]}]}"
|
||||||
},
|
},
|
||||||
"headersSize": -1,
|
"headersSize": -1,
|
||||||
"bodySize": -1,
|
"bodySize": -1,
|
||||||
|
@ -124,7 +124,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"size": -1,
|
"size": -1,
|
||||||
"mimeType": "application/json",
|
"mimeType": "application/json",
|
||||||
"text": "{\"count\":6,\"next\":null,\"previous\":null,\"all\":[8,17,7,4,11,15],\"results\":[{\"id\":8,\"name\":\"Correspondent 2\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":3,\"value\":\"2\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":17,\"name\":\"In the Last Month\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":20,\"value\":\"created:[-1 month to now]\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":7,\"name\":\"Inbox\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"9\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":4,\"name\":\"Recently Added\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[],\"owner\":\"2\",\"user_can_change\":true},{\"id\":11,\"name\":\"Tag: Another Sample Tag\",\"show_on_dashboard\":false,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"4\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":15,\"name\":\"View ASN not empty\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":18,\"value\":\"false\"}],\"owner\":\"2\",\"user_can_change\":true}]}"
|
"text": "{\"count\":6,\"next\":null,\"previous\":null,\"all\":[8,17,7,4,11,15],\"results\":[{\"id\":8,\"name\":\"Correspondent 2\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":3,\"value\":\"2\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":17,\"name\":\"In the Last Month\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":20,\"value\":\"created:[-1 month to now]\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":7,\"name\":\"Inbox\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"9\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":4,\"name\":\"Recently Added\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":11,\"name\":\"Tag: Another Sample Tag\",\"show_on_dashboard\":false,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"4\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":15,\"name\":\"View ASN not empty\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":18,\"value\":\"false\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]}]}"
|
||||||
},
|
},
|
||||||
"headersSize": -1,
|
"headersSize": -1,
|
||||||
"bodySize": -1,
|
"bodySize": -1,
|
||||||
|
@ -124,7 +124,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"size": -1,
|
"size": -1,
|
||||||
"mimeType": "application/json",
|
"mimeType": "application/json",
|
||||||
"text": "{\"count\":6,\"next\":null,\"previous\":null,\"all\":[8,17,7,4,11,15],\"results\":[{\"id\":8,\"name\":\"Correspondent 2\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":3,\"value\":\"2\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":7,\"name\":\"Inbox\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"9\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":11,\"name\":\"Tag: Another Sample Tag\",\"show_on_dashboard\":false,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"4\"}],\"owner\":\"2\",\"user_can_change\":true}]}"
|
"text": "{\"count\":6,\"next\":null,\"previous\":null,\"all\":[8,17,7,4,11,15],\"results\":[{\"id\":8,\"name\":\"Correspondent 2\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":3,\"value\":\"2\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":17,\"name\":\"In the Last Month\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":20,\"value\":\"created:[-1 month to now]\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":7,\"name\":\"Inbox\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"9\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":4,\"name\":\"Recently Added\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":11,\"name\":\"Tag: Another Sample Tag\",\"show_on_dashboard\":false,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"4\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]},{\"id\":15,\"name\":\"View ASN not empty\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":18,\"value\":\"false\"}],\"owner\":\"2\",\"user_can_change\":true,\"page_size\":10,\"display_mode\":\"table\",\"display_fields\":[\"created\",\"title\",\"tag\",\"documenttype\"]}]}"
|
||||||
},
|
},
|
||||||
"headersSize": -1,
|
"headersSize": -1,
|
||||||
"bodySize": -1,
|
"bodySize": -1,
|
||||||
|
@ -138,11 +138,11 @@ test('sorting', async ({ page }) => {
|
|||||||
test('change views', async ({ page }) => {
|
test('change views', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR5, { notFound: 'fallback' })
|
await page.routeFromHAR(REQUESTS_HAR5, { notFound: 'fallback' })
|
||||||
await page.goto('/documents')
|
await page.goto('/documents')
|
||||||
await page.locator('pngx-page-header label').first().click()
|
await page.locator('.btn-group label').first().click()
|
||||||
await expect(page.locator('pngx-document-list table')).toBeVisible()
|
await expect(page.locator('pngx-document-list table')).toBeVisible()
|
||||||
await page.locator('pngx-page-header label').nth(1).click()
|
await page.locator('.btn-group label').nth(1).click()
|
||||||
await expect(page.locator('pngx-document-card-small').first()).toBeAttached()
|
await expect(page.locator('pngx-document-card-small').first()).toBeAttached()
|
||||||
await page.locator('pngx-page-header label').nth(2).click()
|
await page.locator('.btn-group label').nth(2).click()
|
||||||
await expect(page.locator('pngx-document-card-large').first()).toBeAttached()
|
await expect(page.locator('pngx-document-card-large').first()).toBeAttached()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -120,6 +120,8 @@ import { RotateConfirmDialogComponent } from './components/common/confirm-dialog
|
|||||||
import { MergeConfirmDialogComponent } from './components/common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component'
|
import { MergeConfirmDialogComponent } from './components/common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component'
|
||||||
import { SplitConfirmDialogComponent } from './components/common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component'
|
import { SplitConfirmDialogComponent } from './components/common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component'
|
||||||
import { DocumentHistoryComponent } from './components/document-history/document-history.component'
|
import { DocumentHistoryComponent } from './components/document-history/document-history.component'
|
||||||
|
import { DragDropSelectComponent } from './components/common/input/drag-drop-select/drag-drop-select.component'
|
||||||
|
import { CustomFieldDisplayComponent } from './components/common/custom-field-display/custom-field-display.component'
|
||||||
import {
|
import {
|
||||||
airplane,
|
airplane,
|
||||||
archive,
|
archive,
|
||||||
@ -139,6 +141,7 @@ import {
|
|||||||
calendar,
|
calendar,
|
||||||
calendarEvent,
|
calendarEvent,
|
||||||
cardChecklist,
|
cardChecklist,
|
||||||
|
cardHeading,
|
||||||
caretDown,
|
caretDown,
|
||||||
caretUp,
|
caretUp,
|
||||||
chatLeftText,
|
chatLeftText,
|
||||||
@ -233,6 +236,7 @@ const icons = {
|
|||||||
calendar,
|
calendar,
|
||||||
calendarEvent,
|
calendarEvent,
|
||||||
cardChecklist,
|
cardChecklist,
|
||||||
|
cardHeading,
|
||||||
caretDown,
|
caretDown,
|
||||||
caretUp,
|
caretUp,
|
||||||
chatLeftText,
|
chatLeftText,
|
||||||
@ -474,6 +478,8 @@ function initializeApp(settings: SettingsService) {
|
|||||||
MergeConfirmDialogComponent,
|
MergeConfirmDialogComponent,
|
||||||
SplitConfirmDialogComponent,
|
SplitConfirmDialogComponent,
|
||||||
DocumentHistoryComponent,
|
DocumentHistoryComponent,
|
||||||
|
DragDropSelectComponent,
|
||||||
|
CustomFieldDisplayComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
|
@ -320,52 +320,71 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 i18n>Views</h4>
|
<h4 i18n>Views</h4>
|
||||||
<div formGroupName="savedViews">
|
<ul class="list-group" formGroupName="savedViews">
|
||||||
|
|
||||||
@for (view of savedViews; track view) {
|
@for (view of savedViews; track view) {
|
||||||
|
<li class="list-group-item py-3">
|
||||||
<div [formGroupName]="view.id" class="row">
|
<div [formGroupName]="view.id" class="row">
|
||||||
<div class="mb-3 col">
|
<div class="row">
|
||||||
<label class="form-label" for="name_{{view.id}}" i18n>Name</label>
|
<div class="col">
|
||||||
<input type="text" class="form-control" formControlName="name" id="name_{{view.id}}">
|
<pngx-input-text title="Name" formControlName="name"></pngx-input-text>
|
||||||
</div>
|
|
||||||
<div class="mb-2 col">
|
|
||||||
<label class="form-label" for="show_on_dashboard_{{view.id}}" i18n> <span class="visually-hidden">Appears on</span></label>
|
|
||||||
<div class="form-check form-switch">
|
|
||||||
<input type="checkbox" class="form-check-input" id="show_on_dashboard_{{view.id}}" formControlName="show_on_dashboard">
|
|
||||||
<label class="form-check-label" for="show_on_dashboard_{{view.id}}" i18n>Show on dashboard</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check form-switch">
|
<div class="col">
|
||||||
<input type="checkbox" class="form-check-input" id="show_in_sidebar_{{view.id}}" formControlName="show_in_sidebar">
|
<div class="form-check form-switch mt-3">
|
||||||
<label class="form-check-label" for="show_in_sidebar_{{view.id}}" i18n>Show in sidebar</label>
|
<input type="checkbox" class="form-check-input" id="show_on_dashboard_{{view.id}}" formControlName="show_on_dashboard">
|
||||||
|
<label class="form-check-label" for="show_on_dashboard_{{view.id}}" i18n>Show on dashboard</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input type="checkbox" class="form-check-input" id="show_in_sidebar_{{view.id}}" formControlName="show_in_sidebar">
|
||||||
|
<label class="form-check-label" for="show_in_sidebar_{{view.id}}" i18n>Show in sidebar</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<label class="form-label" for="name_{{view.id}}" i18n>Actions</label>
|
||||||
|
<pngx-confirm-button
|
||||||
|
label="Delete"
|
||||||
|
i18n-label
|
||||||
|
(confirm)="deleteSavedView(view)"
|
||||||
|
*pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.SavedView }"
|
||||||
|
buttonClasses="btn-sm btn-outline-danger form-control"
|
||||||
|
iconName="trash">
|
||||||
|
</pngx-confirm-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2 col-auto">
|
<div class="row">
|
||||||
<label class="form-label" for="name_{{view.id}}" i18n>Actions</label>
|
<div class="col">
|
||||||
|
<pngx-input-number i18n-title title="Documents page size" [showAdd]="false" formControlName="page_size"></pngx-input-number>
|
||||||
<pngx-confirm-button
|
</div>
|
||||||
label="Delete"
|
<div class="col">
|
||||||
i18n-label
|
<label class="form-label" for="display_mode_{{view.id}}" i18n>Display as</label>
|
||||||
(confirm)="deleteSavedView(view)"
|
<select class="form-select" formControlName="display_mode">
|
||||||
*pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.SavedView }"
|
<option [ngValue]="DisplayMode.TABLE" i18n>Table</option>
|
||||||
buttonClasses="btn-sm btn-outline-danger form-control"
|
<option [ngValue]="DisplayMode.SMALL_CARDS" i18n>Small Cards</option>
|
||||||
iconName="trash">
|
<option [ngValue]="DisplayMode.LARGE_CARDS" i18n>Large Cards</option>
|
||||||
</pngx-confirm-button>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
@if (displayFields) {
|
||||||
|
<pngx-input-drag-drop-select i18n-title title="Show" i18n-emptyText emptyText="Default" [items]="displayFields" formControlName="display_fields"></pngx-input-drag-drop-select>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</li>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (savedViews && savedViews.length === 0) {
|
@if (savedViews && savedViews.length === 0) {
|
||||||
<div i18n>No saved views defined.</div>
|
<li class="list-group-item">
|
||||||
|
<div i18n>No saved views defined.</div>
|
||||||
|
</li>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (!savedViews) {
|
@if (!savedViews) {
|
||||||
<div>
|
<li class="list-group-item">
|
||||||
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||||
<div class="visually-hidden" i18n>Loading...</div>
|
<div class="visually-hidden" i18n>Loading...</div>
|
||||||
</div>
|
</li>
|
||||||
}
|
}
|
||||||
|
|
||||||
</div>
|
</ul>
|
||||||
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
@ -374,4 +393,5 @@
|
|||||||
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
|
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
|
<button type="submit" class="btn btn-primary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
|
||||||
|
<button type="button" (click)="reset()" class="btn btn-secondary ms-2 mb-2" [disabled]="(isDirty$ | async) === false" i18n>Cancel</button>
|
||||||
</form>
|
</form>
|
||||||
|
@ -48,6 +48,8 @@ import {
|
|||||||
InstallType,
|
InstallType,
|
||||||
SystemStatusItemStatus,
|
SystemStatusItemStatus,
|
||||||
} from 'src/app/data/system-status'
|
} from 'src/app/data/system-status'
|
||||||
|
import { DragDropSelectComponent } from '../../common/input/drag-drop-select/drag-drop-select.component'
|
||||||
|
import { DragDropModule } from '@angular/cdk/drag-drop'
|
||||||
|
|
||||||
const savedViews = [
|
const savedViews = [
|
||||||
{ id: 1, name: 'view1', show_in_sidebar: true, show_on_dashboard: true },
|
{ id: 1, name: 'view1', show_in_sidebar: true, show_on_dashboard: true },
|
||||||
@ -96,6 +98,7 @@ describe('SettingsComponent', () => {
|
|||||||
PermissionsGroupComponent,
|
PermissionsGroupComponent,
|
||||||
IfOwnerDirective,
|
IfOwnerDirective,
|
||||||
ConfirmButtonComponent,
|
ConfirmButtonComponent,
|
||||||
|
DragDropSelectComponent,
|
||||||
],
|
],
|
||||||
providers: [CustomDatePipe, DatePipe, PermissionsGuard],
|
providers: [CustomDatePipe, DatePipe, PermissionsGuard],
|
||||||
imports: [
|
imports: [
|
||||||
@ -108,6 +111,7 @@ describe('SettingsComponent', () => {
|
|||||||
NgSelectModule,
|
NgSelectModule,
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
NgbModalModule,
|
NgbModalModule,
|
||||||
|
DragDropModule,
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
@ -437,4 +441,11 @@ describe('SettingsComponent', () => {
|
|||||||
size: 'xl',
|
size: 'xl',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should support reset', () => {
|
||||||
|
completeSetup()
|
||||||
|
component.settingsForm.get('themeColor').setValue('#ff0000')
|
||||||
|
component.reset()
|
||||||
|
expect(component.settingsForm.get('themeColor').value).toEqual('')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -50,6 +50,7 @@ import {
|
|||||||
SystemStatusItemStatus,
|
SystemStatusItemStatus,
|
||||||
SystemStatus,
|
SystemStatus,
|
||||||
} from 'src/app/data/system-status'
|
} from 'src/app/data/system-status'
|
||||||
|
import { DisplayMode } from 'src/app/data/document'
|
||||||
|
|
||||||
enum SettingsNavIDs {
|
enum SettingsNavIDs {
|
||||||
General = 1,
|
General = 1,
|
||||||
@ -73,8 +74,8 @@ export class SettingsComponent
|
|||||||
extends ComponentWithPermissions
|
extends ComponentWithPermissions
|
||||||
implements OnInit, AfterViewInit, OnDestroy, DirtyComponent
|
implements OnInit, AfterViewInit, OnDestroy, DirtyComponent
|
||||||
{
|
{
|
||||||
SettingsNavIDs = SettingsNavIDs
|
|
||||||
activeNavID: number
|
activeNavID: number
|
||||||
|
DisplayMode = DisplayMode
|
||||||
|
|
||||||
savedViewGroup = new FormGroup({})
|
savedViewGroup = new FormGroup({})
|
||||||
|
|
||||||
@ -110,6 +111,10 @@ export class SettingsComponent
|
|||||||
})
|
})
|
||||||
|
|
||||||
savedViews: SavedView[]
|
savedViews: SavedView[]
|
||||||
|
SettingsNavIDs = SettingsNavIDs
|
||||||
|
get displayFields() {
|
||||||
|
return this.settings.allDisplayFields
|
||||||
|
}
|
||||||
|
|
||||||
store: BehaviorSubject<any>
|
store: BehaviorSubject<any>
|
||||||
storeSub: Subscription
|
storeSub: Subscription
|
||||||
@ -340,6 +345,9 @@ export class SettingsComponent
|
|||||||
name: view.name,
|
name: view.name,
|
||||||
show_on_dashboard: view.show_on_dashboard,
|
show_on_dashboard: view.show_on_dashboard,
|
||||||
show_in_sidebar: view.show_in_sidebar,
|
show_in_sidebar: view.show_in_sidebar,
|
||||||
|
page_size: view.page_size,
|
||||||
|
display_mode: view.display_mode,
|
||||||
|
display_fields: view.display_fields,
|
||||||
}
|
}
|
||||||
this.savedViewGroup.addControl(
|
this.savedViewGroup.addControl(
|
||||||
view.id.toString(),
|
view.id.toString(),
|
||||||
@ -348,6 +356,9 @@ export class SettingsComponent
|
|||||||
name: new FormControl(null),
|
name: new FormControl(null),
|
||||||
show_on_dashboard: new FormControl(null),
|
show_on_dashboard: new FormControl(null),
|
||||||
show_in_sidebar: new FormControl(null),
|
show_in_sidebar: new FormControl(null),
|
||||||
|
page_size: new FormControl(null),
|
||||||
|
display_mode: new FormControl(null),
|
||||||
|
display_fields: new FormControl([]),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -530,8 +541,8 @@ export class SettingsComponent
|
|||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.store.next(this.settingsForm.value)
|
this.store.next(this.settingsForm.value)
|
||||||
this.documentListViewService.updatePageSize()
|
|
||||||
this.settings.updateAppearanceSettings()
|
this.settings.updateAppearanceSettings()
|
||||||
|
this.settings.initializeDisplayFields()
|
||||||
let savedToast: Toast = {
|
let savedToast: Toast = {
|
||||||
content: $localize`Settings were saved successfully.`,
|
content: $localize`Settings were saved successfully.`,
|
||||||
delay: 5000,
|
delay: 5000,
|
||||||
@ -592,6 +603,10 @@ export class SettingsComponent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.settingsForm.patchValue(this.store.getValue())
|
||||||
|
}
|
||||||
|
|
||||||
clearThemeColor() {
|
clearThemeColor() {
|
||||||
this.settingsForm.get('themeColor').patchValue('')
|
this.settingsForm.get('themeColor').patchValue('')
|
||||||
}
|
}
|
||||||
|
@ -111,7 +111,7 @@
|
|||||||
</h6>
|
</h6>
|
||||||
}
|
}
|
||||||
<ul class="nav flex-column mb-2" cdkDropList (cdkDropListDropped)="onDrop($event)">
|
<ul class="nav flex-column mb-2" cdkDropList (cdkDropListDropped)="onDrop($event)">
|
||||||
@for (view of savedViewService.sidebarViews; track view) {
|
@for (view of savedViewService.sidebarViews; track view.id) {
|
||||||
<li class="nav-item w-100 app-link" cdkDrag [cdkDragDisabled]="!settingsService.organizingSidebarSavedViews"
|
<li class="nav-item w-100 app-link" cdkDrag [cdkDragDisabled]="!settingsService.organizingSidebarSavedViews"
|
||||||
cdkDragPreviewContainer="parent" cdkDragPreviewClass="navItemDrag" (cdkDragStarted)="onDragStart($event)"
|
cdkDragPreviewContainer="parent" cdkDragPreviewClass="navItemDrag" (cdkDragStarted)="onDragStart($event)"
|
||||||
(cdkDragEnded)="onDragEnd($event)">
|
(cdkDragEnded)="onDragEnd($event)">
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
@if (field) {
|
||||||
|
@switch (field.data_type) {
|
||||||
|
@case (CustomFieldDataType.Monetary) {
|
||||||
|
<span>{{value | currency: currency}}</span>
|
||||||
|
}
|
||||||
|
@case (CustomFieldDataType.Date) {
|
||||||
|
<span>{{value | customDate}}</span>
|
||||||
|
}
|
||||||
|
@case (CustomFieldDataType.Url) {
|
||||||
|
<a [href]="value" class="btn-link text-dark text-decoration-none" target="_blank">{{value}}</a>
|
||||||
|
}
|
||||||
|
@case (CustomFieldDataType.DocumentLink) {
|
||||||
|
<div class="d-flex gap-1 flex-wrap">
|
||||||
|
@for (docId of value; track docId) {
|
||||||
|
<a routerLink="/documents/{{docId}}" class="badge bg-dark text-primary" title="View" i18n-title>
|
||||||
|
<i-bs width="0.9em" height="0.9em" name="file-text"></i-bs> <span>{{ getDocumentTitle(docId) }}</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@default {
|
||||||
|
<span>{{value}}</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
import { of } from 'rxjs'
|
||||||
|
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||||
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
|
import { CustomFieldDisplayComponent } from './custom-field-display.component'
|
||||||
|
import { DisplayField, Document } from 'src/app/data/document'
|
||||||
|
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||||
|
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||||
|
|
||||||
|
const customFields: CustomField[] = [
|
||||||
|
{ id: 1, name: 'Field 1', data_type: CustomFieldDataType.String },
|
||||||
|
{ id: 2, name: 'Field 2', data_type: CustomFieldDataType.Monetary },
|
||||||
|
{ id: 3, name: 'Field 3', data_type: CustomFieldDataType.DocumentLink },
|
||||||
|
]
|
||||||
|
const document: Document = {
|
||||||
|
id: 1,
|
||||||
|
title: 'Doc 1',
|
||||||
|
custom_fields: [
|
||||||
|
{ field: 1, document: 1, created: null, value: 'Text value' },
|
||||||
|
{ field: 2, document: 1, created: null, value: '100 USD' },
|
||||||
|
{ field: 3, document: 1, created: null, value: '1,2,3' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('CustomFieldDisplayComponent', () => {
|
||||||
|
let component: CustomFieldDisplayComponent
|
||||||
|
let fixture: ComponentFixture<CustomFieldDisplayComponent>
|
||||||
|
let documentService: DocumentService
|
||||||
|
let customFieldService: CustomFieldsService
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [CustomFieldDisplayComponent],
|
||||||
|
providers: [DocumentService],
|
||||||
|
imports: [HttpClientTestingModule],
|
||||||
|
}).compileComponents()
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
documentService = TestBed.inject(DocumentService)
|
||||||
|
customFieldService = TestBed.inject(CustomFieldsService)
|
||||||
|
jest
|
||||||
|
.spyOn(customFieldService, 'listAll')
|
||||||
|
.mockReturnValue(of({ results: customFields } as any))
|
||||||
|
fixture = TestBed.createComponent(CustomFieldDisplayComponent)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should initialize component', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(documentService, 'getFew')
|
||||||
|
.mockReturnValue(of({ results: [] } as any))
|
||||||
|
|
||||||
|
component.fieldDisplayKey = DisplayField.CUSTOM_FIELD + '2'
|
||||||
|
expect(component.fieldId).toEqual(2)
|
||||||
|
component.document = document
|
||||||
|
expect(component.document.title).toEqual('Doc 1')
|
||||||
|
|
||||||
|
expect(component.field).toEqual(customFields[1])
|
||||||
|
expect(component.value).toEqual(100)
|
||||||
|
expect(component.currency).toEqual('USD')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should get document titles', () => {
|
||||||
|
const docLinkDocuments: Document[] = [
|
||||||
|
{ id: 1, title: 'Document 1' } as any,
|
||||||
|
{ id: 2, title: 'Document 2' } as any,
|
||||||
|
{ id: 3, title: 'Document 3' } as any,
|
||||||
|
]
|
||||||
|
jest
|
||||||
|
.spyOn(documentService, 'getFew')
|
||||||
|
.mockReturnValue(of({ results: docLinkDocuments } as any))
|
||||||
|
component.fieldId = 3
|
||||||
|
component.document = document
|
||||||
|
|
||||||
|
const title1 = component.getDocumentTitle(1)
|
||||||
|
const title2 = component.getDocumentTitle(2)
|
||||||
|
const title3 = component.getDocumentTitle(3)
|
||||||
|
|
||||||
|
expect(title1).toEqual('Document 1')
|
||||||
|
expect(title2).toEqual('Document 2')
|
||||||
|
expect(title3).toEqual('Document 3')
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,105 @@
|
|||||||
|
import { Component, Input, OnDestroy, OnInit } from '@angular/core'
|
||||||
|
import { Subject, takeUntil } from 'rxjs'
|
||||||
|
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||||
|
import { DisplayField, Document } from 'src/app/data/document'
|
||||||
|
import { Results } from 'src/app/data/results'
|
||||||
|
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||||
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pngx-custom-field-display',
|
||||||
|
templateUrl: './custom-field-display.component.html',
|
||||||
|
styleUrl: './custom-field-display.component.scss',
|
||||||
|
})
|
||||||
|
export class CustomFieldDisplayComponent implements OnInit, OnDestroy {
|
||||||
|
CustomFieldDataType = CustomFieldDataType
|
||||||
|
|
||||||
|
private _document: Document
|
||||||
|
@Input()
|
||||||
|
set document(document: Document) {
|
||||||
|
this._document = document
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
get document(): Document {
|
||||||
|
return this._document
|
||||||
|
}
|
||||||
|
|
||||||
|
private _fieldId: number
|
||||||
|
@Input()
|
||||||
|
set fieldId(id: number) {
|
||||||
|
this._fieldId = id
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
get fieldId(): number {
|
||||||
|
return this._fieldId
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set fieldDisplayKey(key: string) {
|
||||||
|
this.fieldId = parseInt(key.replace(DisplayField.CUSTOM_FIELD, ''), 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
value: any
|
||||||
|
currency: string
|
||||||
|
|
||||||
|
private customFields: CustomField[] = []
|
||||||
|
|
||||||
|
public field: CustomField
|
||||||
|
|
||||||
|
private docLinkDocuments: Document[] = []
|
||||||
|
|
||||||
|
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private customFieldService: CustomFieldsService,
|
||||||
|
private documentService: DocumentService
|
||||||
|
) {
|
||||||
|
this.customFieldService.listAll().subscribe((r) => {
|
||||||
|
this.customFields = r.results
|
||||||
|
this.init()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
if (this.value || !this._fieldId || !this._document || !this.customFields) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.field = this.customFields.find((f) => f.id === this._fieldId)
|
||||||
|
this.value = this._document.custom_fields.find(
|
||||||
|
(f) => f.field === this._fieldId
|
||||||
|
)?.value
|
||||||
|
if (this.value && this.field.data_type === CustomFieldDataType.Monetary) {
|
||||||
|
this.currency = this.value.match(/([A-Z]{3})/)?.[0]
|
||||||
|
this.value = parseFloat(this.value.replace(this.currency, ''))
|
||||||
|
} else if (
|
||||||
|
this.value?.length &&
|
||||||
|
this.field.data_type === CustomFieldDataType.DocumentLink
|
||||||
|
) {
|
||||||
|
this.getDocuments()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDocuments() {
|
||||||
|
this.documentService
|
||||||
|
.getFew(this.value, { fields: 'id,title' })
|
||||||
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe((result: Results<Document>) => {
|
||||||
|
this.docLinkDocuments = result.results
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public getDocumentTitle(docId: number): string {
|
||||||
|
return this.docLinkDocuments?.find((d) => d.id === docId)?.title
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.unsubscribeNotifier.next(true)
|
||||||
|
this.unsubscribeNotifier.complete()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
<div class="d-flex flex-row mt-2 align-items-center">
|
||||||
|
<span class="me-2">{{title}}:</span>
|
||||||
|
<div class="d-flex flex-row gap-2 w-100 mh-1" style="min-height: 1em;"
|
||||||
|
cdkDropList #selectedList="cdkDropList"
|
||||||
|
cdkDropListOrientation="horizontal"
|
||||||
|
(cdkDropListDropped)="drop($event)"
|
||||||
|
[cdkDropListConnectedTo]="[unselectedList]">
|
||||||
|
@for (item of selectedItems; track item.id) {
|
||||||
|
<div class="badge bg-primary" cdkDrag>{{item.name}}</div>
|
||||||
|
}
|
||||||
|
@if (selectedItems.length === 0) {
|
||||||
|
<div class="badge bg-light fst-italic" i18n>{{emptyText}}</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-row mt-2 align-items-center bg-light p-2">
|
||||||
|
<div class="d-flex flex-row gap-2 w-100 mh-1" style="min-height: 1em;"
|
||||||
|
cdkDropList #unselectedList="cdkDropList"
|
||||||
|
cdkDropListOrientation="horizontal"
|
||||||
|
(cdkDropListDropped)="drop($event)"
|
||||||
|
[cdkDropListConnectedTo]="[selectedList]">
|
||||||
|
@for (item of unselectedItems; track item.id) {
|
||||||
|
<div class="badge bg-secondary opacity-50" cdkDrag>{{item.name}}</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,7 @@
|
|||||||
|
.badge {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-flex {
|
||||||
|
overflow-x: scroll;
|
||||||
|
}
|
@ -0,0 +1,102 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
import { DragDropModule } from '@angular/cdk/drag-drop'
|
||||||
|
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||||
|
import { DragDropSelectComponent } from './drag-drop-select.component'
|
||||||
|
|
||||||
|
describe('DragDropSelectComponent', () => {
|
||||||
|
let component: DragDropSelectComponent
|
||||||
|
let fixture: ComponentFixture<DragDropSelectComponent>
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [DragDropModule, FormsModule],
|
||||||
|
declarations: [DragDropSelectComponent],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(DragDropSelectComponent)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update selectedItems when writeValue is called', () => {
|
||||||
|
const newValue = ['1', '2', '3']
|
||||||
|
component.items = [
|
||||||
|
{ id: '1', name: 'Item 1' },
|
||||||
|
{ id: '2', name: 'Item 2' },
|
||||||
|
{ id: '3', name: 'Item 3' },
|
||||||
|
]
|
||||||
|
component.writeValue(newValue)
|
||||||
|
expect(component.selectedItems).toEqual([
|
||||||
|
{ id: '1', name: 'Item 1' },
|
||||||
|
{ id: '2', name: 'Item 2' },
|
||||||
|
{ id: '3', name: 'Item 3' },
|
||||||
|
])
|
||||||
|
|
||||||
|
component.writeValue(null)
|
||||||
|
expect(component.selectedItems).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update selectedItems when an item is dropped within selectedList', () => {
|
||||||
|
component.items = [
|
||||||
|
{ id: '1', name: 'Item 1' },
|
||||||
|
{ id: '2', name: 'Item 2' },
|
||||||
|
{ id: '3', name: 'Item 3' },
|
||||||
|
{ id: '4', name: 'Item 4' },
|
||||||
|
]
|
||||||
|
component.writeValue(['1', '2', '3'])
|
||||||
|
const event = {
|
||||||
|
previousContainer: component.selectedList,
|
||||||
|
container: component.selectedList,
|
||||||
|
previousIndex: 1,
|
||||||
|
currentIndex: 2,
|
||||||
|
}
|
||||||
|
component.drop(event as any)
|
||||||
|
expect(component.selectedItems).toEqual([
|
||||||
|
{ id: '1', name: 'Item 1' },
|
||||||
|
{ id: '3', name: 'Item 3' },
|
||||||
|
{ id: '2', name: 'Item 2' },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update selectedItems when an item is dropped from unselectedList to selectedList', () => {
|
||||||
|
component.items = [
|
||||||
|
{ id: '1', name: 'Item 1' },
|
||||||
|
{ id: '2', name: 'Item 2' },
|
||||||
|
{ id: '3', name: 'Item 3' },
|
||||||
|
]
|
||||||
|
component.writeValue(['1', '2'])
|
||||||
|
const event = {
|
||||||
|
previousContainer: component.unselectedList,
|
||||||
|
container: component.selectedList,
|
||||||
|
previousIndex: 0,
|
||||||
|
currentIndex: 2,
|
||||||
|
}
|
||||||
|
component.drop(event as any)
|
||||||
|
expect(component.selectedItems).toEqual([
|
||||||
|
{ id: '1', name: 'Item 1' },
|
||||||
|
{ id: '2', name: 'Item 2' },
|
||||||
|
{ id: '3', name: 'Item 3' },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update selectedItems when an item is dropped from selectedList to unselectedList', () => {
|
||||||
|
component.items = [
|
||||||
|
{ id: '1', name: 'Item 1' },
|
||||||
|
{ id: '2', name: 'Item 2' },
|
||||||
|
{ id: '3', name: 'Item 3' },
|
||||||
|
]
|
||||||
|
component.writeValue(['1', '2', '3'])
|
||||||
|
const event = {
|
||||||
|
previousContainer: component.selectedList,
|
||||||
|
container: component.unselectedList,
|
||||||
|
previousIndex: 1,
|
||||||
|
currentIndex: 0,
|
||||||
|
}
|
||||||
|
component.drop(event as any)
|
||||||
|
expect(component.selectedItems).toEqual([
|
||||||
|
{ id: '1', name: 'Item 1' },
|
||||||
|
{ id: '3', name: 'Item 3' },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,68 @@
|
|||||||
|
import { Component, Input, ViewChild, forwardRef } from '@angular/core'
|
||||||
|
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||||
|
import { AbstractInputComponent } from '../abstract-input'
|
||||||
|
import {
|
||||||
|
CdkDragDrop,
|
||||||
|
CdkDropList,
|
||||||
|
moveItemInArray,
|
||||||
|
} from '@angular/cdk/drag-drop'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
useExisting: forwardRef(() => DragDropSelectComponent),
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selector: 'pngx-input-drag-drop-select',
|
||||||
|
templateUrl: './drag-drop-select.component.html',
|
||||||
|
styleUrl: './drag-drop-select.component.scss',
|
||||||
|
})
|
||||||
|
export class DragDropSelectComponent extends AbstractInputComponent<string[]> {
|
||||||
|
@Input() title: string = $localize`Selected items`
|
||||||
|
|
||||||
|
@Input() items: { id: string; name: string }[] = []
|
||||||
|
public selectedItems: { id: string; name: string }[] = []
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
emptyText = $localize`No items selected`
|
||||||
|
|
||||||
|
@ViewChild('selectedList') selectedList: CdkDropList
|
||||||
|
@ViewChild('unselectedList') unselectedList: CdkDropList
|
||||||
|
|
||||||
|
get unselectedItems(): { id: string; name: string }[] {
|
||||||
|
return this.items.filter((i) => !this.selectedItems.includes(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
writeValue(newValue: string[]): void {
|
||||||
|
super.writeValue(newValue)
|
||||||
|
this.selectedItems =
|
||||||
|
newValue?.map((id) => this.items.find((i) => i.id === id)) ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
public drop(event: CdkDragDrop<string[]>) {
|
||||||
|
if (
|
||||||
|
event.previousContainer === event.container &&
|
||||||
|
event.container === this.selectedList
|
||||||
|
) {
|
||||||
|
moveItemInArray(
|
||||||
|
this.selectedItems,
|
||||||
|
event.previousIndex,
|
||||||
|
event.currentIndex
|
||||||
|
)
|
||||||
|
} else if (event.container === this.selectedList) {
|
||||||
|
this.selectedItems.splice(
|
||||||
|
event.currentIndex,
|
||||||
|
0,
|
||||||
|
this.unselectedItems[event.previousIndex]
|
||||||
|
)
|
||||||
|
} else if (
|
||||||
|
event.container === this.unselectedList &&
|
||||||
|
event.previousContainer === this.selectedList
|
||||||
|
) {
|
||||||
|
this.selectedItems.splice(event.previousIndex, 1)
|
||||||
|
}
|
||||||
|
this.onChange(this.selectedItems.map((i) => i.id))
|
||||||
|
}
|
||||||
|
}
|
@ -23,7 +23,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
|
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
|
||||||
@for (v of dashboardViews; track v) {
|
@for (v of dashboardViews; track v.id) {
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<pngx-saved-view-widget
|
<pngx-saved-view-widget
|
||||||
[savedView]="v"
|
[savedView]="v"
|
||||||
|
@ -9,58 +9,114 @@
|
|||||||
<a class="btn-link text-decoration-none" header-buttons [routerLink]="[]" (click)="showAll()" i18n>Show all</a>
|
<a class="btn-link text-decoration-none" header-buttons [routerLink]="[]" (click)="showAll()" i18n>Show all</a>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (documents.length) {
|
@if (documents.length && displayMode === DisplayMode.TABLE) {
|
||||||
<table content class="table table-hover mb-0 align-middle">
|
<table content class="table table-hover mb-0 mt-n2 align-middle">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" i18n>Created</th>
|
@for (field of displayFields; track field; let i = $index) {
|
||||||
<th scope="col" i18n>Title</th>
|
@if (displayFields.includes(field)) {
|
||||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Tag)) {
|
<th
|
||||||
<th scope="col" class="d-none d-md-table-cell" i18n>Tags</th>
|
scope="col"
|
||||||
}
|
[ngClass]="{
|
||||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
|
'd-none d-md-table-cell': i > 1,
|
||||||
<th scope="col" class="d-none d-md-table-cell" i18n>Correspondent</th>
|
'w-25': field === DisplayField.CREATED || field === DisplayField.ADDED
|
||||||
} @else {
|
}">
|
||||||
<th scope="col" class="d-none d-md-table-cell"></th>
|
{{ getColumnTitle(field) }}
|
||||||
|
</th>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (doc of documents; track doc) {
|
@for (doc of documents; track doc.id) {
|
||||||
<tr (mouseleave)="maybeClosePopover()">
|
<tr>
|
||||||
<td class="py-2 py-md-3"><a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.created_date | customDate}}</a></td>
|
@for (field of displayFields; track field; let i = $index) {
|
||||||
<td class="py-2 py-md-3">
|
<td class="py-2 py-md-3 position-relative" [ngClass]="{ 'd-none d-md-table-cell': i > 1 }">
|
||||||
<a routerLink="/documents/{{doc.id}}" title="Edit" i18n-title class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.title | documentTitle}}</a>
|
@switch (field) {
|
||||||
</td>
|
@case (DisplayField.ADDED) {
|
||||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Tag)) {
|
<a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.added | customDate}}</a>
|
||||||
<td class="py-2 py-md-3 d-none d-md-table-cell">
|
}
|
||||||
@for (t of doc.tags$ | async; track t) {
|
@case (DisplayField.CREATED) {
|
||||||
<pngx-tag [tag]="t" class="ms-1" (click)="clickTag(t, $event)"></pngx-tag>
|
<a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.created_date | customDate}}</a>
|
||||||
|
}
|
||||||
|
@case (DisplayField.TITLE) {
|
||||||
|
<a routerLink="/documents/{{doc.id}}" title="Edit" i18n-title class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.title | documentTitle}}</a>
|
||||||
|
}
|
||||||
|
@case (DisplayField.CORRESPONDENT) {
|
||||||
|
@if (doc.correspondent) {
|
||||||
|
<a class="btn-link text-dark text-decoration-none" type="button" (click)="clickCorrespondent(doc.correspondent, $event)">{{(doc.correspondent$ | async)?.name}}</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@case (DisplayField.TAGS) {
|
||||||
|
@for (t of doc.tags$ | async; track t) {
|
||||||
|
<pngx-tag [tag]="t" class="ms-1" (click)="clickTag(t.id, $event)"></pngx-tag>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@case (DisplayField.DOCUMENT_TYPE) {
|
||||||
|
@if (doc.document_type) {
|
||||||
|
<a class="btn-link text-dark text-decoration-none" type="button" (click)="clickDocType(doc.document_type, $event)">{{(doc.document_type$ | async)?.name}}</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@case (DisplayField.STORAGE_PATH) {
|
||||||
|
@if (doc.storage_path) {
|
||||||
|
<a class="btn-link text-dark text-decoration-none" type="button" (click)="clickStoragePath(doc.storage_path, $event)">{{(doc.storage_path$ | async)?.name}}</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@if (field.startsWith(DisplayField.CUSTOM_FIELD)) {
|
||||||
|
<pngx-custom-field-display [document]="doc" [fieldDisplayKey]="field"></pngx-custom-field-display>
|
||||||
|
}
|
||||||
|
@if (i === displayFields.length - 1) {
|
||||||
|
<div class="btn-group position-absolute top-50 end-0 translate-middle-y">
|
||||||
|
<a [href]="getPreviewUrl(doc)" title="View Preview" i18n-title target="_blank" class="btn px-4 btn-dark border-dark-subtle"
|
||||||
|
[ngbPopover]="previewContent" [popoverTitle]="doc.title | documentTitle"
|
||||||
|
autoClose="true" popoverClass="shadow popover-preview" container="body" (mouseenter)="mouseEnterPreviewButton(doc)" (mouseleave)="mouseLeavePreviewButton()" #popover="ngbPopover">
|
||||||
|
<i-bs width="0.8em" height="0.8em" name="eye"></i-bs>
|
||||||
|
</a>
|
||||||
|
<ng-template #previewContent>
|
||||||
|
<pngx-preview-popup [document]="doc" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()"></pngx-preview-popup>
|
||||||
|
</ng-template>
|
||||||
|
<a [href]="getDownloadUrl(doc)" class="btn px-4 btn-dark border-dark-subtle" title="Download" i18n-title (click)="$event.stopPropagation()">
|
||||||
|
<i-bs width="0.8em" height="0.8em" name="download"></i-bs>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
}
|
}
|
||||||
<td class="position-relative py-2 py-md-3 d-none d-md-table-cell">
|
|
||||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Correspondent) && doc.correspondent !== null) {
|
|
||||||
<a class="btn-link text-dark text-decoration-none py-2 py-md-3" routerLink="/documents" [queryParams]="getCorrespondentQueryParams(doc.correspondent)">{{(doc.correspondent$ | async)?.name}}</a>
|
|
||||||
}
|
|
||||||
<div class="btn-group position-absolute top-50 end-0 translate-middle-y">
|
|
||||||
<a [href]="getPreviewUrl(doc)" title="View Preview" i18n-title target="_blank" class="btn px-4 btn-dark border-dark-subtle"
|
|
||||||
[ngbPopover]="previewContent" [popoverTitle]="doc.title | documentTitle"
|
|
||||||
autoClose="true" popoverClass="shadow popover-preview" container="body" (mouseenter)="mouseEnterPreviewButton(doc)" (mouseleave)="mouseLeavePreviewButton()" #popover="ngbPopover">
|
|
||||||
<i-bs width="0.8em" height="0.8em" name="eye"></i-bs>
|
|
||||||
</a>
|
|
||||||
<ng-template #previewContent>
|
|
||||||
<pngx-preview-popup [document]="doc" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()"></pngx-preview-popup>
|
|
||||||
</ng-template>
|
|
||||||
<a [href]="getDownloadUrl(doc)" class="btn px-4 btn-dark border-dark-subtle" title="Download" i18n-title (click)="$event.stopPropagation()">
|
|
||||||
<i-bs width="0.8em" height="0.8em" name="download"></i-bs>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
} @else if (documents.length && displayMode === DisplayMode.SMALL_CARDS) {
|
||||||
|
<div class="row row-cols-paperless-cards my-n2">
|
||||||
|
@for (d of documents; track d.id) {
|
||||||
|
<pngx-document-card-small
|
||||||
|
class="p-0"
|
||||||
|
(dblClickDocument)="openDocumentDetail(d)"
|
||||||
|
[document]="d"
|
||||||
|
[displayFields]="displayFields"
|
||||||
|
(clickTag)="clickTag($event)"
|
||||||
|
(clickCorrespondent)="clickCorrespondent($event)"
|
||||||
|
(clickStoragePath)="clickStoragePath($event)"
|
||||||
|
(clickDocumentType)="clickDocumentType($event)">
|
||||||
|
</pngx-document-card-small>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
} @else if (documents.length && displayMode === DisplayMode.LARGE_CARDS) {
|
||||||
|
<div class="row my-n2">
|
||||||
|
@for (d of documents; track d.id) {
|
||||||
|
<pngx-document-card-large
|
||||||
|
(dblClickDocument)="openDocumentDetail(d)"
|
||||||
|
[document]="d"
|
||||||
|
[displayFields]="displayFields"
|
||||||
|
(clickTag)="clickTag($event)"
|
||||||
|
(clickCorrespondent)="clickCorrespondent($event)"
|
||||||
|
(clickStoragePath)="clickStoragePath($event)"
|
||||||
|
(clickDocumentType)="clickDocumentType($event)"
|
||||||
|
(clickMoreLike)="clickMoreLike(d.id)">
|
||||||
|
</pngx-document-card-large>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<p i18n class="text-center text-muted mb-0 fst-italic">No documents</p>
|
<p i18n class="text-center text-muted mb-0 fst-italic">No documents</p>
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,9 @@ table {
|
|||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
th:first-child {
|
@media (min-width: 768px) {
|
||||||
width: 25%;
|
th.w-25 {
|
||||||
@media (min-width: 768px) {
|
width: 15% !important;
|
||||||
width: 15%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,3 +29,45 @@ td.py-3 {
|
|||||||
padding-top: 0.75em !important;
|
padding-top: 0.75em !important;
|
||||||
padding-bottom: 0.75em !important;
|
padding-bottom: 0.75em !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$paperless-card-breakpoints: (
|
||||||
|
// 0: 2, // xs is manual for slim-sidebar
|
||||||
|
768px: 2, //md
|
||||||
|
992px: 2, //lg
|
||||||
|
1200px: 3, //xl
|
||||||
|
1600px: 4,
|
||||||
|
1800px: 5,
|
||||||
|
2000px: 6
|
||||||
|
);
|
||||||
|
|
||||||
|
.row-cols-paperless-cards {
|
||||||
|
// xs, we dont want in .col-slim block
|
||||||
|
> * {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: calc(100% / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@each $width, $n_cols in $paperless-card-breakpoints {
|
||||||
|
@media(min-width: $width) {
|
||||||
|
> * {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: calc(100% / $n-cols);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .col-slim .row-cols-paperless-cards {
|
||||||
|
@each $width, $n_cols in $paperless-card-breakpoints {
|
||||||
|
@media(min-width: $width) {
|
||||||
|
> * {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: calc(100% / ($n-cols + 1)) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .document-card-check {
|
||||||
|
display: none !important; // override for dashboard
|
||||||
|
}
|
||||||
|
@ -11,7 +11,13 @@ import { RouterTestingModule } from '@angular/router/testing'
|
|||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { of, Subject } from 'rxjs'
|
import { of, Subject } from 'rxjs'
|
||||||
import { routes } from 'src/app/app-routing.module'
|
import { routes } from 'src/app/app-routing.module'
|
||||||
import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type'
|
import {
|
||||||
|
FILTER_CORRESPONDENT,
|
||||||
|
FILTER_DOCUMENT_TYPE,
|
||||||
|
FILTER_FULLTEXT_MORELIKE,
|
||||||
|
FILTER_HAS_TAGS_ALL,
|
||||||
|
FILTER_STORAGE_PATH,
|
||||||
|
} from 'src/app/data/filter-rule-type'
|
||||||
import { SavedView } from 'src/app/data/saved-view'
|
import { SavedView } from 'src/app/data/saved-view'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||||
@ -31,6 +37,10 @@ import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
|
|||||||
import { DragDropModule } from '@angular/cdk/drag-drop'
|
import { DragDropModule } from '@angular/cdk/drag-drop'
|
||||||
import { PreviewPopupComponent } from 'src/app/components/common/preview-popup/preview-popup.component'
|
import { PreviewPopupComponent } from 'src/app/components/common/preview-popup/preview-popup.component'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||||
|
import { CustomFieldDataType } from 'src/app/data/custom-field'
|
||||||
|
import { CustomFieldDisplayComponent } from 'src/app/components/common/custom-field-display/custom-field-display.component'
|
||||||
|
import { DisplayMode, DisplayField } from 'src/app/data/document'
|
||||||
|
|
||||||
const savedView: SavedView = {
|
const savedView: SavedView = {
|
||||||
id: 1,
|
id: 1,
|
||||||
@ -45,17 +55,53 @@ const savedView: SavedView = {
|
|||||||
value: '1,2',
|
value: '1,2',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
page_size: 20,
|
||||||
|
display_mode: DisplayMode.TABLE,
|
||||||
|
display_fields: [
|
||||||
|
DisplayField.CREATED,
|
||||||
|
DisplayField.TITLE,
|
||||||
|
DisplayField.TAGS,
|
||||||
|
DisplayField.CORRESPONDENT,
|
||||||
|
DisplayField.DOCUMENT_TYPE,
|
||||||
|
DisplayField.STORAGE_PATH,
|
||||||
|
`${DisplayField.CUSTOM_FIELD}11` as any,
|
||||||
|
`${DisplayField.CUSTOM_FIELD}15` as any,
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
const documentResults = [
|
const documentResults = [
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: 'doc2',
|
title: 'doc2',
|
||||||
|
custom_fields: [
|
||||||
|
{ id: 1, field: 11, created: new Date(), value: 'custom', document: 2 },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
title: 'doc3',
|
title: 'doc3',
|
||||||
correspondent: 0,
|
correspondent: 0,
|
||||||
|
custom_fields: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: 'doc4',
|
||||||
|
custom_fields: [
|
||||||
|
{ id: 32, field: 3, created: new Date(), value: 'EUR123', document: 4 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title: 'doc5',
|
||||||
|
custom_fields: [
|
||||||
|
{
|
||||||
|
id: 22,
|
||||||
|
field: 15,
|
||||||
|
created: new Date(),
|
||||||
|
value: [123, 456, 789],
|
||||||
|
document: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -77,6 +123,7 @@ describe('SavedViewWidgetComponent', () => {
|
|||||||
DocumentTitlePipe,
|
DocumentTitlePipe,
|
||||||
SafeUrlPipe,
|
SafeUrlPipe,
|
||||||
PreviewPopupComponent,
|
PreviewPopupComponent,
|
||||||
|
CustomFieldDisplayComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
PermissionsGuard,
|
PermissionsGuard,
|
||||||
@ -89,6 +136,33 @@ describe('SavedViewWidgetComponent', () => {
|
|||||||
},
|
},
|
||||||
CustomDatePipe,
|
CustomDatePipe,
|
||||||
DatePipe,
|
DatePipe,
|
||||||
|
{
|
||||||
|
provide: CustomFieldsService,
|
||||||
|
useValue: {
|
||||||
|
listAll: () =>
|
||||||
|
of({
|
||||||
|
all: [3, 11, 15],
|
||||||
|
count: 3,
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Custom field 3',
|
||||||
|
data_type: CustomFieldDataType.Monetary,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
name: 'Custom Field 11',
|
||||||
|
data_type: CustomFieldDataType.String,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 15,
|
||||||
|
name: 'Custom Field 15',
|
||||||
|
data_type: CustomFieldDataType.DocumentLink,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
HttpClientTestingModule,
|
HttpClientTestingModule,
|
||||||
@ -170,7 +244,7 @@ describe('SavedViewWidgetComponent', () => {
|
|||||||
component.ngOnInit()
|
component.ngOnInit()
|
||||||
expect(listAllSpy).toHaveBeenCalledWith(
|
expect(listAllSpy).toHaveBeenCalledWith(
|
||||||
1,
|
1,
|
||||||
10,
|
20,
|
||||||
savedView.sort_field,
|
savedView.sort_field,
|
||||||
savedView.sort_reverse,
|
savedView.sort_reverse,
|
||||||
savedView.filter_rules,
|
savedView.filter_rules,
|
||||||
@ -204,11 +278,78 @@ describe('SavedViewWidgetComponent', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should navigate to document', () => {
|
||||||
|
const routerSpy = jest.spyOn(router, 'navigate')
|
||||||
|
component.openDocumentDetail(documentResults[0])
|
||||||
|
expect(routerSpy).toHaveBeenCalledWith(['documents', documentResults[0].id])
|
||||||
|
})
|
||||||
|
|
||||||
it('should navigate via quickfilter on click tag', () => {
|
it('should navigate via quickfilter on click tag', () => {
|
||||||
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
||||||
component.clickTag({ id: 11, name: 'Tag11' }, new MouseEvent('click'))
|
component.clickTag(11, new MouseEvent('click'))
|
||||||
expect(qfSpy).toHaveBeenCalledWith([
|
expect(qfSpy).toHaveBeenCalledWith([
|
||||||
{ rule_type: FILTER_HAS_TAGS_ALL, value: '11' },
|
{ rule_type: FILTER_HAS_TAGS_ALL, value: '11' },
|
||||||
])
|
])
|
||||||
|
component.clickTag(11) // coverage
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should navigate via quickfilter on click correspondent', () => {
|
||||||
|
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
||||||
|
component.clickCorrespondent(11, new MouseEvent('click'))
|
||||||
|
expect(qfSpy).toHaveBeenCalledWith([
|
||||||
|
{ rule_type: FILTER_CORRESPONDENT, value: '11' },
|
||||||
|
])
|
||||||
|
component.clickCorrespondent(11) // coverage
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should navigate via quickfilter on click doc type', () => {
|
||||||
|
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
||||||
|
component.clickDocType(11, new MouseEvent('click'))
|
||||||
|
expect(qfSpy).toHaveBeenCalledWith([
|
||||||
|
{ rule_type: FILTER_DOCUMENT_TYPE, value: '11' },
|
||||||
|
])
|
||||||
|
component.clickDocType(11) // coverage
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should navigate via quickfilter on click storage path', () => {
|
||||||
|
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
||||||
|
component.clickStoragePath(11, new MouseEvent('click'))
|
||||||
|
expect(qfSpy).toHaveBeenCalledWith([
|
||||||
|
{ rule_type: FILTER_STORAGE_PATH, value: '11' },
|
||||||
|
])
|
||||||
|
component.clickStoragePath(11) // coverage
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should navigate via quickfilter on click more like', () => {
|
||||||
|
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
||||||
|
component.clickMoreLike(11)
|
||||||
|
expect(qfSpy).toHaveBeenCalledWith([
|
||||||
|
{ rule_type: FILTER_FULLTEXT_MORELIKE, value: '11' },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should get correct column title', () => {
|
||||||
|
expect(component.getColumnTitle(DisplayField.TITLE)).toEqual('Title')
|
||||||
|
expect(component.getColumnTitle(DisplayField.CREATED)).toEqual('Created')
|
||||||
|
expect(component.getColumnTitle(DisplayField.ADDED)).toEqual('Added')
|
||||||
|
expect(component.getColumnTitle(DisplayField.TAGS)).toEqual('Tags')
|
||||||
|
expect(component.getColumnTitle(DisplayField.CORRESPONDENT)).toEqual(
|
||||||
|
'Correspondent'
|
||||||
|
)
|
||||||
|
expect(component.getColumnTitle(DisplayField.DOCUMENT_TYPE)).toEqual(
|
||||||
|
'Document type'
|
||||||
|
)
|
||||||
|
expect(component.getColumnTitle(DisplayField.STORAGE_PATH)).toEqual(
|
||||||
|
'Storage path'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should get correct column title for custom field', () => {
|
||||||
|
expect(
|
||||||
|
component.getColumnTitle((DisplayField.CUSTOM_FIELD + 11) as any)
|
||||||
|
).toEqual('Custom Field 11')
|
||||||
|
expect(
|
||||||
|
component.getColumnTitle((DisplayField.CUSTOM_FIELD + 15) as any)
|
||||||
|
).toEqual('Custom Field 15')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -6,23 +6,38 @@ import {
|
|||||||
QueryList,
|
QueryList,
|
||||||
ViewChildren,
|
ViewChildren,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { Params, Router } from '@angular/router'
|
import { Router } from '@angular/router'
|
||||||
import { Subject, takeUntil } from 'rxjs'
|
import { Subject, takeUntil } from 'rxjs'
|
||||||
import { Document } from 'src/app/data/document'
|
import {
|
||||||
|
DEFAULT_DASHBOARD_DISPLAY_FIELDS,
|
||||||
|
DEFAULT_DASHBOARD_VIEW_PAGE_SIZE,
|
||||||
|
DEFAULT_DISPLAY_FIELDS,
|
||||||
|
DisplayField,
|
||||||
|
DisplayMode,
|
||||||
|
Document,
|
||||||
|
} from 'src/app/data/document'
|
||||||
import { SavedView } from 'src/app/data/saved-view'
|
import { SavedView } from 'src/app/data/saved-view'
|
||||||
import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
|
import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
import { Tag } from 'src/app/data/tag'
|
|
||||||
import {
|
import {
|
||||||
FILTER_CORRESPONDENT,
|
FILTER_CORRESPONDENT,
|
||||||
|
FILTER_DOCUMENT_TYPE,
|
||||||
|
FILTER_FULLTEXT_MORELIKE,
|
||||||
FILTER_HAS_TAGS_ALL,
|
FILTER_HAS_TAGS_ALL,
|
||||||
|
FILTER_STORAGE_PATH,
|
||||||
} from 'src/app/data/filter-rule-type'
|
} from 'src/app/data/filter-rule-type'
|
||||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||||
import { ComponentWithPermissions } from 'src/app/components/with-permissions/with-permissions.component'
|
import { ComponentWithPermissions } from 'src/app/components/with-permissions/with-permissions.component'
|
||||||
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { queryParamsFromFilterRules } from 'src/app/utils/query-params'
|
import {
|
||||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
PermissionAction,
|
||||||
|
PermissionType,
|
||||||
|
PermissionsService,
|
||||||
|
} from 'src/app/services/permissions.service'
|
||||||
|
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||||
|
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||||
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-saved-view-widget',
|
selector: 'pngx-saved-view-widget',
|
||||||
@ -33,8 +48,14 @@ export class SavedViewWidgetComponent
|
|||||||
extends ComponentWithPermissions
|
extends ComponentWithPermissions
|
||||||
implements OnInit, OnDestroy
|
implements OnInit, OnDestroy
|
||||||
{
|
{
|
||||||
|
public DisplayMode = DisplayMode
|
||||||
|
public DisplayField = DisplayField
|
||||||
|
public CustomFieldDataType = CustomFieldDataType
|
||||||
|
|
||||||
loading: boolean = true
|
loading: boolean = true
|
||||||
|
|
||||||
|
private customFields: CustomField[] = []
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private documentService: DocumentService,
|
private documentService: DocumentService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
@ -42,7 +63,9 @@ export class SavedViewWidgetComponent
|
|||||||
private consumerStatusService: ConsumerStatusService,
|
private consumerStatusService: ConsumerStatusService,
|
||||||
public openDocumentsService: OpenDocumentsService,
|
public openDocumentsService: OpenDocumentsService,
|
||||||
public documentListViewService: DocumentListViewService,
|
public documentListViewService: DocumentListViewService,
|
||||||
public permissionsService: PermissionsService
|
public permissionsService: PermissionsService,
|
||||||
|
private settingsService: SettingsService,
|
||||||
|
private customFieldService: CustomFieldsService
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
@ -60,14 +83,44 @@ export class SavedViewWidgetComponent
|
|||||||
mouseOnPreview = false
|
mouseOnPreview = false
|
||||||
popoverHidden = true
|
popoverHidden = true
|
||||||
|
|
||||||
|
displayMode: DisplayMode
|
||||||
|
|
||||||
|
displayFields: DisplayField[] = DEFAULT_DASHBOARD_DISPLAY_FIELDS
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.reload()
|
this.reload()
|
||||||
|
this.displayMode = this.savedView.display_mode ?? DisplayMode.TABLE
|
||||||
this.consumerStatusService
|
this.consumerStatusService
|
||||||
.onDocumentConsumptionFinished()
|
.onDocumentConsumptionFinished()
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
this.reload()
|
this.reload()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.View,
|
||||||
|
PermissionType.CustomField
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.customFieldService
|
||||||
|
.listAll()
|
||||||
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe((customFields) => {
|
||||||
|
this.customFields = customFields.results
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.savedView.display_fields) {
|
||||||
|
this.displayFields = this.savedView.display_fields
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter by perms etc
|
||||||
|
this.displayFields = this.displayFields.filter(
|
||||||
|
(field) =>
|
||||||
|
this.settingsService.allDisplayFields.find((f) => f.id === field) !==
|
||||||
|
undefined
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
@ -80,7 +133,7 @@ export class SavedViewWidgetComponent
|
|||||||
this.documentService
|
this.documentService
|
||||||
.listFiltered(
|
.listFiltered(
|
||||||
1,
|
1,
|
||||||
10,
|
this.savedView.page_size ?? DEFAULT_DASHBOARD_VIEW_PAGE_SIZE,
|
||||||
this.savedView.sort_field,
|
this.savedView.sort_field,
|
||||||
this.savedView.sort_reverse,
|
this.savedView.sort_reverse,
|
||||||
this.savedView.filter_rules,
|
this.savedView.filter_rules,
|
||||||
@ -103,15 +156,52 @@ export class SavedViewWidgetComponent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clickTag(tag: Tag, event: MouseEvent) {
|
clickTag(tagID: number, event: MouseEvent = null) {
|
||||||
event.preventDefault()
|
event?.preventDefault()
|
||||||
event.stopImmediatePropagation()
|
event?.stopImmediatePropagation()
|
||||||
|
|
||||||
this.list.quickFilter([
|
this.list.quickFilter([
|
||||||
{ rule_type: FILTER_HAS_TAGS_ALL, value: tag.id.toString() },
|
{ rule_type: FILTER_HAS_TAGS_ALL, value: tagID.toString() },
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clickCorrespondent(correspondentId: number, event: MouseEvent = null) {
|
||||||
|
event?.preventDefault()
|
||||||
|
event?.stopImmediatePropagation()
|
||||||
|
|
||||||
|
this.list.quickFilter([
|
||||||
|
{ rule_type: FILTER_CORRESPONDENT, value: correspondentId.toString() },
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
clickDocType(docTypeId: number, event: MouseEvent = null) {
|
||||||
|
event?.preventDefault()
|
||||||
|
event?.stopImmediatePropagation()
|
||||||
|
|
||||||
|
this.list.quickFilter([
|
||||||
|
{ rule_type: FILTER_DOCUMENT_TYPE, value: docTypeId.toString() },
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
clickStoragePath(storagePathId: number, event: MouseEvent = null) {
|
||||||
|
event?.preventDefault()
|
||||||
|
event?.stopImmediatePropagation()
|
||||||
|
|
||||||
|
this.list.quickFilter([
|
||||||
|
{ rule_type: FILTER_STORAGE_PATH, value: storagePathId.toString() },
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
clickMoreLike(documentID: number) {
|
||||||
|
this.list.quickFilter([
|
||||||
|
{ rule_type: FILTER_FULLTEXT_MORELIKE, value: documentID.toString() },
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
openDocumentDetail(document: Document) {
|
||||||
|
this.router.navigate(['documents', document.id])
|
||||||
|
}
|
||||||
|
|
||||||
getPreviewUrl(document: Document): string {
|
getPreviewUrl(document: Document): string {
|
||||||
return this.documentService.getPreviewUrl(document.id)
|
return this.documentService.getPreviewUrl(document.id)
|
||||||
}
|
}
|
||||||
@ -161,14 +251,11 @@ export class SavedViewWidgetComponent
|
|||||||
}, 300)
|
}, 300)
|
||||||
}
|
}
|
||||||
|
|
||||||
getCorrespondentQueryParams(correspondentId: number): Params {
|
public getColumnTitle(field: DisplayField): string {
|
||||||
return correspondentId !== undefined
|
if (field.startsWith(DisplayField.CUSTOM_FIELD)) {
|
||||||
? queryParamsFromFilterRules([
|
const id = field.split('_')[2]
|
||||||
{
|
return this.customFields.find((f) => f.id === parseInt(id))?.name
|
||||||
rule_type: FILTER_CORRESPONDENT,
|
}
|
||||||
value: correspondentId.toString(),
|
return DEFAULT_DISPLAY_FIELDS.find((f) => f.id === field)?.name
|
||||||
},
|
|
||||||
])
|
|
||||||
: null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,11 +13,11 @@
|
|||||||
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||||
<div class="visually-hidden" i18n>Loading...</div>
|
<div class="visually-hidden" i18n>Loading...</div>
|
||||||
}
|
}
|
||||||
<ng-content select ="[header-buttons]"></ng-content>
|
<ng-content select="[header-buttons]"></ng-content>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body text-dark">
|
<div class="card-body text-dark">
|
||||||
<ng-content select ="[content]"></ng-content>
|
<ng-content select="[content]"></ng-content>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<h5 class="card-title">
|
<h5 class="card-title">
|
||||||
@if (document.correspondent) {
|
@if (displayFields.includes(DisplayField.CORRESPONDENT) && document.correspondent) {
|
||||||
@if (clickCorrespondent.observers.length ) {
|
@if (clickCorrespondent.observers.length ) {
|
||||||
<a title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name}}</a>
|
<a title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name}}</a>
|
||||||
} @else {
|
} @else {
|
||||||
@ -23,14 +23,18 @@
|
|||||||
}
|
}
|
||||||
:
|
:
|
||||||
}
|
}
|
||||||
{{document.title | documentTitle}}
|
@if (displayFields.includes(DisplayField.TITLE)) {
|
||||||
@for (t of document.tags$ | async; track t) {
|
{{document.title | documentTitle}}
|
||||||
<pngx-tag [tag]="t" linkTitle="Filter by tag" i18n-linkTitle class="ms-1" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="clickTag.observers.length"></pngx-tag>
|
}
|
||||||
|
@if (displayFields.includes(DisplayField.TAGS)) {
|
||||||
|
@for (t of document.tags$ | async; track t) {
|
||||||
|
<pngx-tag [tag]="t" linkTitle="Filter by tag" i18n-linkTitle class="ms-1" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="clickTag.observers.length"></pngx-tag>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<p class="card-text">
|
<p class="card-text">
|
||||||
@if (document.__search_hit__ && document.__search_hit__.highlights) {
|
@if (document.__search_hit__?.score && document.__search_hit__.highlights) {
|
||||||
<span [innerHtml]="document.__search_hit__.highlights"></span>
|
<span [innerHtml]="document.__search_hit__.highlights"></span>
|
||||||
}
|
}
|
||||||
@for (highlight of searchNoteHighlights; track highlight) {
|
@for (highlight of searchNoteHighlights; track highlight) {
|
||||||
@ -39,7 +43,7 @@
|
|||||||
<span [innerHtml]="highlight"></span>
|
<span [innerHtml]="highlight"></span>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@if (!document.__search_hit__) {
|
@if (!document.__search_hit__?.score) {
|
||||||
<span class="result-content">{{contentTrimmed}}</span>
|
<span class="result-content">{{contentTrimmed}}</span>
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
@ -66,44 +70,53 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="list-group list-group-horizontal border-0 card-info ms-md-auto mt-2 mt-md-0">
|
<div class="list-group list-group-horizontal border-0 card-info ms-md-auto mt-2 mt-md-0">
|
||||||
@if (notesEnabled && document.notes.length) {
|
@if (displayFields.includes(DisplayField.NOTES) && notesEnabled && document.notes.length) {
|
||||||
<button routerLink="/documents/{{document.id}}/notes" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2 d-flex align-items-center" title="View notes" i18n-title>
|
<button routerLink="/documents/{{document.id}}/notes" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2 d-flex align-items-center" title="View notes" i18n-title>
|
||||||
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="chat-left-text"></i-bs><small i18n>{{document.notes.length}} Notes</small>
|
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="chat-left-text"></i-bs><small i18n>{{document.notes.length}} Notes</small>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@if (document.document_type) {
|
@if (displayFields.includes(DisplayField.DOCUMENT_TYPE) && document.document_type) {
|
||||||
<button type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2 d-flex align-items-center" title="Filter by document type" i18n-title
|
<button type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2 d-flex align-items-center" title="Filter by document type" i18n-title
|
||||||
(click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
|
(click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
|
||||||
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="file-earmark"></i-bs><small>{{(document.document_type$ | async)?.name}}</small>
|
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="file-earmark"></i-bs><small>{{(document.document_type$ | async)?.name}}</small>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@if (document.storage_path) {
|
@if (displayFields.includes(DisplayField.STORAGE_PATH) && document.storage_path) {
|
||||||
<button type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2 d-flex align-items-center" title="Filter by storage path" i18n-title
|
<button type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2 d-flex align-items-center" title="Filter by storage path" i18n-title
|
||||||
(click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
|
(click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
|
||||||
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="archive"></i-bs><small>{{(document.storage_path$ | async)?.name}}</small>
|
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="archive"></i-bs><small>{{(document.storage_path$ | async)?.name}}</small>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@if (document.archive_serial_number | isNumber) {
|
@if (displayFields.includes(DisplayField.ASN) && document.archive_serial_number | isNumber) {
|
||||||
<div class="list-group-item me-2 bg-light text-dark p-1 border-0 d-flex align-items-center">
|
<div class="list-group-item me-2 bg-light text-dark p-1 border-0 d-flex align-items-center">
|
||||||
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="upc-scan"></i-bs><small>#{{document.archive_serial_number}}</small>
|
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="upc-scan"></i-bs><small>#{{document.archive_serial_number}}</small>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<ng-template #dateTooltip>
|
@if (displayFields.includes(DisplayField.CREATED) || displayFields.includes(DisplayField.ADDED)) {
|
||||||
<div class="d-flex flex-column text-light">
|
<ng-template #dateTooltip>
|
||||||
<span i18n>Created: {{ document.created | customDate }}</span>
|
<div class="d-flex flex-column text-light">
|
||||||
<span i18n>Added: {{ document.added | customDate }}</span>
|
<span i18n>Created: {{ document.created | customDate }}</span>
|
||||||
<span i18n>Modified: {{ document.modified | customDate }}</span>
|
<span i18n>Added: {{ document.added | customDate }}</span>
|
||||||
</div>
|
<span i18n>Modified: {{ document.modified | customDate }}</span>
|
||||||
</ng-template>
|
</div>
|
||||||
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center" [ngbTooltip]="dateTooltip">
|
</ng-template>
|
||||||
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="calendar-event"></i-bs><small>{{document.created_date | customDate:'mediumDate'}}</small>
|
@if (displayFields.includes(DisplayField.CREATED)) {
|
||||||
</div>
|
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center" [ngbTooltip]="dateTooltip">
|
||||||
@if (document.owner && document.owner !== settingsService.currentUser.id) {
|
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="calendar-event"></i-bs><small>{{document.created_date | customDate:'mediumDate'}}</small>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (displayFields.includes(DisplayField.ADDED)) {
|
||||||
|
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center" [ngbTooltip]="dateTooltip">
|
||||||
|
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="calendar-event"></i-bs><small>{{document.added | customDate:'mediumDate'}}</small>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@if (displayFields.includes(DisplayField.OWNER) && document.owner && document.owner !== settingsService.currentUser.id) {
|
||||||
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center">
|
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center">
|
||||||
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="person-fill-lock"></i-bs><small>{{document.owner | username}}</small>
|
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="person-fill-lock"></i-bs><small>{{document.owner | username}}</small>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (document.is_shared_by_requester) {
|
@if (displayFields.includes(DisplayField.SHARED) && document.is_shared_by_requester) {
|
||||||
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center">
|
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center">
|
||||||
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="people-fill"></i-bs><small i18n>Shared</small>
|
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="people-fill"></i-bs><small i18n>Shared</small>
|
||||||
</div>
|
</div>
|
||||||
@ -114,6 +127,16 @@
|
|||||||
<ngb-progressbar [type]="searchScoreClass" [value]="document.__search_hit__.score" class="search-score-bar mx-2 mt-1" [max]="1"></ngb-progressbar>
|
<ngb-progressbar [type]="searchScoreClass" [value]="document.__search_hit__.score" class="search-score-bar mx-2 mt-1" [max]="1"></ngb-progressbar>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@for (field of document.custom_fields; track field.id) {
|
||||||
|
@if (displayFields.includes(DisplayField.CUSTOM_FIELD + field.field)) {
|
||||||
|
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center">
|
||||||
|
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="ui-radios"></i-bs>
|
||||||
|
<small>
|
||||||
|
<pngx-custom-field-display [document]="document" [fieldId]="field.field"></pngx-custom-field-display>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,6 +21,7 @@ import { DocumentCardLargeComponent } from './document-card-large.component'
|
|||||||
import { IsNumberPipe } from 'src/app/pipes/is-number.pipe'
|
import { IsNumberPipe } from 'src/app/pipes/is-number.pipe'
|
||||||
import { PreviewPopupComponent } from '../../common/preview-popup/preview-popup.component'
|
import { PreviewPopupComponent } from '../../common/preview-popup/preview-popup.component'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
import { CustomFieldDisplayComponent } from '../../common/custom-field-display/custom-field-display.component'
|
||||||
|
|
||||||
const doc = {
|
const doc = {
|
||||||
id: 10,
|
id: 10,
|
||||||
@ -53,6 +54,7 @@ describe('DocumentCardLargeComponent', () => {
|
|||||||
SafeUrlPipe,
|
SafeUrlPipe,
|
||||||
IsNumberPipe,
|
IsNumberPipe,
|
||||||
PreviewPopupComponent,
|
PreviewPopupComponent,
|
||||||
|
CustomFieldDisplayComponent,
|
||||||
],
|
],
|
||||||
providers: [DatePipe],
|
providers: [DatePipe],
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -5,7 +5,11 @@ import {
|
|||||||
Output,
|
Output,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { Document } from 'src/app/data/document'
|
import {
|
||||||
|
DEFAULT_DISPLAY_FIELDS,
|
||||||
|
DisplayField,
|
||||||
|
Document,
|
||||||
|
} from 'src/app/data/document'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
||||||
@ -18,6 +22,8 @@ import { ComponentWithPermissions } from '../../with-permissions/with-permission
|
|||||||
styleUrls: ['./document-card-large.component.scss'],
|
styleUrls: ['./document-card-large.component.scss'],
|
||||||
})
|
})
|
||||||
export class DocumentCardLargeComponent extends ComponentWithPermissions {
|
export class DocumentCardLargeComponent extends ComponentWithPermissions {
|
||||||
|
DisplayField = DisplayField
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private documentService: DocumentService,
|
private documentService: DocumentService,
|
||||||
public settingsService: SettingsService
|
public settingsService: SettingsService
|
||||||
@ -28,6 +34,9 @@ export class DocumentCardLargeComponent extends ComponentWithPermissions {
|
|||||||
@Input()
|
@Input()
|
||||||
selected = false
|
selected = false
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
displayFields: string[] = DEFAULT_DISPLAY_FIELDS.map((f) => f.id)
|
||||||
|
|
||||||
@Output()
|
@Output()
|
||||||
toggleSelected = new EventEmitter()
|
toggleSelected = new EventEmitter()
|
||||||
|
|
||||||
|
@ -10,19 +10,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tags d-flex flex-column text-end position-absolute me-1 fs-6">
|
@if (displayFields?.includes(DisplayField.TAGS)) {
|
||||||
@for (t of getTagsLimited$() | async; track t) {
|
<div class="tags d-flex flex-column text-end position-absolute me-1 fs-6">
|
||||||
<pngx-tag [tag]="t" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="true" linkTitle="Toggle tag filter" i18n-linkTitle></pngx-tag>
|
@for (t of getTagsLimited$() | async; track t) {
|
||||||
}
|
<pngx-tag [tag]="t" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="true" linkTitle="Toggle tag filter" i18n-linkTitle></pngx-tag>
|
||||||
@if (moreTags) {
|
}
|
||||||
<div>
|
@if (moreTags) {
|
||||||
<span class="badge text-dark">+ {{moreTags}}</span>
|
<div>
|
||||||
</div>
|
<span class="badge text-dark">+ {{moreTags}}</span>
|
||||||
}
|
</div>
|
||||||
</div>
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (notesEnabled && document.notes.length) {
|
@if (displayFields.includes(DisplayField.NOTES) && notesEnabled && document.notes.length) {
|
||||||
<a routerLink="/documents/{{document.id}}/notes" class="document-card-notes py-2 px-1">
|
<a routerLink="/documents/{{document.id}}/notes" class="document-card-notes py-2 px-1">
|
||||||
<span class="badge rounded-pill bg-light border text-primary">
|
<span class="badge rounded-pill bg-light border text-primary">
|
||||||
<i-bs width="1.2em" height="1.2em" class="ms-1 me-1" name="chat-left-text"></i-bs>
|
<i-bs width="1.2em" height="1.2em" class="ms-1 me-1" name="chat-left-text"></i-bs>
|
||||||
@ -32,59 +34,86 @@
|
|||||||
|
|
||||||
<div class="card-body bg-light p-2">
|
<div class="card-body bg-light p-2">
|
||||||
<p class="card-text">
|
<p class="card-text">
|
||||||
@if (document.correspondent) {
|
@if (displayFields.includes(DisplayField.CORRESPONDENT) && document.correspondent) {
|
||||||
<a title="Toggle correspondent filter" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name ?? privateName}}</a>:
|
<a title="Toggle correspondent filter" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name ?? privateName}}</a>:
|
||||||
}
|
}
|
||||||
{{document.title | documentTitle}}
|
@if (displayFields.includes(DisplayField.TITLE)) {
|
||||||
|
{{document.title | documentTitle}}
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer pt-0 pb-2 px-2">
|
<div class="card-footer pt-0 pb-2 px-2">
|
||||||
<div class="list-group list-group-flush border-0 pt-1 pb-2 card-info">
|
<div class="list-group list-group-flush border-0 pt-1 pb-2 card-info">
|
||||||
@if (document.document_type) {
|
@if (displayFields.includes(DisplayField.DOCUMENT_TYPE) && document.document_type) {
|
||||||
<button type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle document type filter" i18n-title
|
<button type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle document type filter" i18n-title
|
||||||
(click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
|
(click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
|
||||||
<i-bs width="1em" height="1em" class="me-2 text-muted" name="file-earmark"></i-bs>
|
<i-bs width="1em" height="1em" class="me-2 text-muted" name="file-earmark"></i-bs>
|
||||||
<small>{{(document.document_type$ | async)?.name ?? privateName}}</small>
|
<small>{{(document.document_type$ | async)?.name ?? privateName}}</small>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@if (document.storage_path) {
|
@if (displayFields.includes(DisplayField.STORAGE_PATH) && document.storage_path) {
|
||||||
<button type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle storage path filter" i18n-title
|
<button type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle storage path filter" i18n-title
|
||||||
(click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
|
(click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
|
||||||
<i-bs width="1em" height="1em" class="me-2 text-muted" name="folder"></i-bs>
|
<i-bs width="1em" height="1em" class="me-2 text-muted" name="folder"></i-bs>
|
||||||
<small>{{(document.storage_path$ | async)?.name ?? privateName}}</small>
|
<small>{{(document.storage_path$ | async)?.name ?? privateName}}</small>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
<div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between">
|
@if (displayFields.includes(DisplayField.CREATED)) {
|
||||||
<ng-template #dateTooltip>
|
<div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between">
|
||||||
<div class="d-flex flex-column text-light">
|
<ng-template #dateTooltip>
|
||||||
<span i18n>Created: {{ document.created | customDate }}</span>
|
<div class="d-flex flex-column text-light">
|
||||||
<span i18n>Added: {{ document.added | customDate }}</span>
|
<span i18n>Created: {{ document.created | customDate }}</span>
|
||||||
<span i18n>Modified: {{ document.modified | customDate }}</span>
|
<span i18n>Added: {{ document.added | customDate }}</span>
|
||||||
|
<span i18n>Modified: {{ document.modified | customDate }}</span>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
<div class="ps-0 p-1" placement="top" [ngbTooltip]="dateTooltip">
|
||||||
|
<i-bs width="1em" height="1em" class="me-2 text-muted" name="calendar-event"></i-bs>
|
||||||
|
<small>{{document.created | customDate:'mediumDate'}}</small>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
|
||||||
<div class="ps-0 p-1" placement="top" [ngbTooltip]="dateTooltip">
|
|
||||||
<i-bs width="1em" height="1em" class="me-2 text-muted" name="calendar-event"></i-bs>
|
|
||||||
<small>{{document.created_date | customDate:'mediumDate'}}</small>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
@if (document.archive_serial_number | isNumber) {
|
@if (displayFields.includes(DisplayField.ADDED)) {
|
||||||
|
<div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between">
|
||||||
|
<ng-template #dateTooltip>
|
||||||
|
<div class="d-flex flex-column text-light">
|
||||||
|
<span i18n>Created: {{ document.created | customDate }}</span>
|
||||||
|
<span i18n>Added: {{ document.added | customDate }}</span>
|
||||||
|
<span i18n>Modified: {{ document.modified | customDate }}</span>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
<div class="ps-0 p-1" placement="top" [ngbTooltip]="dateTooltip">
|
||||||
|
<i-bs width="1em" height="1em" class="me-2 text-muted" name="calendar-event"></i-bs>
|
||||||
|
<small>{{document.added | customDate:'mediumDate'}}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (displayFields.includes(DisplayField.ASN) && document.archive_serial_number | isNumber) {
|
||||||
<div class="ps-0 p-1">
|
<div class="ps-0 p-1">
|
||||||
<i-bs width="1em" height="1em" class="me-2 text-muted" name="upc-scan"></i-bs>
|
<i-bs width="1em" height="1em" class="me-2 text-muted" name="upc-scan"></i-bs>
|
||||||
<small>#{{document.archive_serial_number}}</small>
|
<small>#{{document.archive_serial_number}}</small>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (document.owner && document.owner !== settingsService.currentUser.id) {
|
@if (displayFields.includes(DisplayField.OWNER) && document.owner && document.owner !== settingsService.currentUser.id) {
|
||||||
<div class="ps-0 p-1">
|
<div class="ps-0 p-1">
|
||||||
<i-bs width="1em" height="1em" class="me-2 text-muted" name="person-fill-lock"></i-bs>
|
<i-bs width="1em" height="1em" class="me-2 text-muted" name="person-fill-lock"></i-bs>
|
||||||
<small>{{document.owner | username}}</small>
|
<small>{{document.owner | username}}</small>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (document.is_shared_by_requester) {
|
@if (displayFields.includes(DisplayField.SHARED) && document.is_shared_by_requester) {
|
||||||
<div class="ps-0 p-1">
|
<div class="ps-0 p-1">
|
||||||
<i-bs width="1em" height="1em" class="me-2 text-muted" name="people-fill"></i-bs>
|
<i-bs width="1em" height="1em" class="me-2 text-muted" name="people-fill"></i-bs>
|
||||||
<small i18n>Shared</small>
|
<small i18n>Shared</small>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@for (field of document.custom_fields; track field.id) {
|
||||||
|
@if (displayFields.includes(DisplayField.CUSTOM_FIELD + field.field)) {
|
||||||
|
<div class="ps-0 p-1 d-flex align-items-center overflow-hidden">
|
||||||
|
<i-bs width="1em" height="1em" class="me-2 text-muted" name="ui-radios"></i-bs>
|
||||||
|
<small><pngx-custom-field-display [document]="document" [fieldId]="field.field"></pngx-custom-field-display></small>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<div class="btn-group w-100">
|
<div class="btn-group w-100">
|
||||||
|
@ -24,6 +24,7 @@ import { Tag } from 'src/app/data/tag'
|
|||||||
import { IsNumberPipe } from 'src/app/pipes/is-number.pipe'
|
import { IsNumberPipe } from 'src/app/pipes/is-number.pipe'
|
||||||
import { PreviewPopupComponent } from '../../common/preview-popup/preview-popup.component'
|
import { PreviewPopupComponent } from '../../common/preview-popup/preview-popup.component'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
import { CustomFieldDisplayComponent } from '../../common/custom-field-display/custom-field-display.component'
|
||||||
|
|
||||||
const doc = {
|
const doc = {
|
||||||
id: 10,
|
id: 10,
|
||||||
@ -67,6 +68,7 @@ describe('DocumentCardSmallComponent', () => {
|
|||||||
TagComponent,
|
TagComponent,
|
||||||
IsNumberPipe,
|
IsNumberPipe,
|
||||||
PreviewPopupComponent,
|
PreviewPopupComponent,
|
||||||
|
CustomFieldDisplayComponent,
|
||||||
],
|
],
|
||||||
providers: [DatePipe],
|
providers: [DatePipe],
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -6,7 +6,11 @@ import {
|
|||||||
ViewChild,
|
ViewChild,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { map } from 'rxjs/operators'
|
import { map } from 'rxjs/operators'
|
||||||
import { Document } from 'src/app/data/document'
|
import {
|
||||||
|
DEFAULT_DISPLAY_FIELDS,
|
||||||
|
DisplayField,
|
||||||
|
Document,
|
||||||
|
} from 'src/app/data/document'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
||||||
@ -19,6 +23,8 @@ import { ComponentWithPermissions } from '../../with-permissions/with-permission
|
|||||||
styleUrls: ['./document-card-small.component.scss'],
|
styleUrls: ['./document-card-small.component.scss'],
|
||||||
})
|
})
|
||||||
export class DocumentCardSmallComponent extends ComponentWithPermissions {
|
export class DocumentCardSmallComponent extends ComponentWithPermissions {
|
||||||
|
DisplayField = DisplayField
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private documentService: DocumentService,
|
private documentService: DocumentService,
|
||||||
public settingsService: SettingsService
|
public settingsService: SettingsService
|
||||||
@ -35,6 +41,9 @@ export class DocumentCardSmallComponent extends ComponentWithPermissions {
|
|||||||
@Input()
|
@Input()
|
||||||
document: Document
|
document: Document
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
displayFields: string[] = DEFAULT_DISPLAY_FIELDS.map((f) => f.id)
|
||||||
|
|
||||||
@Output()
|
@Output()
|
||||||
dblClickDocument = new EventEmitter()
|
dblClickDocument = new EventEmitter()
|
||||||
|
|
||||||
|
@ -11,16 +11,32 @@
|
|||||||
<button ngbDropdownItem (click)="list.selectAll()" i18n>Select all</button>
|
<button ngbDropdownItem (click)="list.selectAll()" i18n>Select all</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div ngbDropdown class="d-flex">
|
||||||
|
<button class="btn btn-sm btn-outline-primary" id="dropdownDisplayFields" ngbDropdownToggle>
|
||||||
|
<i-bs name="card-heading"></i-bs>
|
||||||
|
<div class="d-none d-sm-inline"> <ng-container i18n>Show</ng-container></div>
|
||||||
|
</button>
|
||||||
|
<div ngbDropdownMenu aria-labelledby="dropdownDisplayFields" class="shadow">
|
||||||
|
<div class="px-3">
|
||||||
|
@for (field of settingsService.allDisplayFields; track field.id) {
|
||||||
|
<div class="form-check my-1">
|
||||||
|
<input class="form-check-input mt-1" type="checkbox" id="displayField{{field.id}}" [checked]="activeDisplayFields.includes(field.id)" (change)="toggleDisplayField(field.id)">
|
||||||
|
<label class="form-check-label" for="displayField{{field.id}}">{{field.name}}</label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="btn-group flex-fill" role="group">
|
<div class="btn-group flex-fill" role="group">
|
||||||
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="details" (ngModelChange)="saveDisplayMode()" id="displayModeDetails" name="displayModeDetails">
|
<input type="radio" class="btn-check" [(ngModel)]="list.displayMode" value="table" id="displayModeDetails" name="displayModeDetails">
|
||||||
<label for="displayModeDetails" class="btn btn-outline-primary btn-sm">
|
<label for="displayModeDetails" class="btn btn-outline-primary btn-sm">
|
||||||
<i-bs name="list-ul"></i-bs>
|
<i-bs name="list-ul"></i-bs>
|
||||||
</label>
|
</label>
|
||||||
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="smallCards" (ngModelChange)="saveDisplayMode()" id="displayModeSmall" name="displayModeSmall">
|
<input type="radio" class="btn-check" [(ngModel)]="list.displayMode" value="smallCards" id="displayModeSmall" name="displayModeSmall">
|
||||||
<label for="displayModeSmall" class="btn btn-outline-primary btn-sm">
|
<label for="displayModeSmall" class="btn btn-outline-primary btn-sm">
|
||||||
<i-bs name="grid"></i-bs>
|
<i-bs name="grid"></i-bs>
|
||||||
</label>
|
</label>
|
||||||
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="largeCards" (ngModelChange)="saveDisplayMode()" id="displayModeLarge" name="displayModeLarge">
|
<input type="radio" class="btn-check" [(ngModel)]="list.displayMode" value="largeCards" id="displayModeLarge" name="displayModeLarge">
|
||||||
<label for="displayModeLarge" class="btn btn-outline-primary btn-sm">
|
<label for="displayModeLarge" class="btn btn-outline-primary btn-sm">
|
||||||
<i-bs name="hdd-stack"></i-bs>
|
<i-bs name="hdd-stack"></i-bs>
|
||||||
</label>
|
</label>
|
||||||
@ -41,7 +57,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@for (f of getSortFields(); track f) {
|
@for (f of getSortFields(); track f) {
|
||||||
<button ngbDropdownItem (click)="setSortField(f.field)"
|
<button ngbDropdownItem (click)="list.sortField = f.field"
|
||||||
[class.active]="list.sortField === f.field">{{f.name}}
|
[class.active]="list.sortField === f.field">{{f.name}}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@ -109,7 +125,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@if (list.collectionSize) {
|
@if (list.collectionSize) {
|
||||||
<ngb-pagination [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
|
<ngb-pagination [pageSize]="list.pageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
|
||||||
[rotate]="true" aria-label="Default pagination" size="sm"></ngb-pagination>
|
[rotate]="true" aria-label="Default pagination" size="sm"></ngb-pagination>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@ -122,26 +138,38 @@
|
|||||||
@if (list.error ) {
|
@if (list.error ) {
|
||||||
<div class="alert alert-danger" role="alert"><ng-container i18n>Error while loading documents</ng-container>: {{list.error}}</div>
|
<div class="alert alert-danger" role="alert"><ng-container i18n>Error while loading documents</ng-container>: {{list.error}}</div>
|
||||||
} @else {
|
} @else {
|
||||||
@if (displayMode === 'largeCards') {
|
@if (list.displayMode === DisplayMode.LARGE_CARDS) {
|
||||||
<div>
|
<div>
|
||||||
@for (d of list.documents; track trackByDocumentId($index, d)) {
|
@for (d of list.documents; track trackByDocumentId($index, d)) {
|
||||||
<pngx-document-card-large [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" (dblClickDocument)="openDocumentDetail(d)" [document]="d" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickDocumentType)="clickDocumentType($event)" (clickStoragePath)="clickStoragePath($event)" (clickMoreLike)="clickMoreLike(d.id)">
|
<pngx-document-card-large
|
||||||
|
[selected]="list.isSelected(d)"
|
||||||
|
(toggleSelected)="toggleSelected(d, $event)"
|
||||||
|
(dblClickDocument)="openDocumentDetail(d)"
|
||||||
|
[document]="d"
|
||||||
|
[displayFields]="activeDisplayFields"
|
||||||
|
(clickTag)="clickTag($event)"
|
||||||
|
(clickCorrespondent)="clickCorrespondent($event)"
|
||||||
|
(clickDocumentType)="clickDocumentType($event)"
|
||||||
|
(clickStoragePath)="clickStoragePath($event)"
|
||||||
|
(clickMoreLike)="clickMoreLike(d.id)">
|
||||||
</pngx-document-card-large>
|
</pngx-document-card-large>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (displayMode === 'details') {
|
@if (list.displayMode === DisplayMode.TABLE) {
|
||||||
<table class="table table-sm align-middle border shadow-sm">
|
<table class="table table-sm align-middle border shadow-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th class="d-none d-lg-table-cell"
|
@if (activeDisplayFields.includes(DisplayField.ASN)) {
|
||||||
pngxSortable="archive_serial_number"
|
<th class="d-none d-lg-table-cell"
|
||||||
title="Sort by ASN" i18n-title
|
pngxSortable="archive_serial_number"
|
||||||
[currentSortField]="list.sortField"
|
title="Sort by ASN" i18n-title
|
||||||
[currentSortReverse]="list.sortReverse"
|
[currentSortField]="list.sortField"
|
||||||
(sort)="onSort($event)"
|
[currentSortReverse]="list.sortReverse"
|
||||||
i18n>ASN</th>
|
(sort)="onSort($event)"
|
||||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
|
i18n>ASN</th>
|
||||||
|
}
|
||||||
|
@if (activeDisplayFields.includes(DisplayField.CORRESPONDENT) && permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
|
||||||
<th class="d-none d-md-table-cell"
|
<th class="d-none d-md-table-cell"
|
||||||
pngxSortable="correspondent__name"
|
pngxSortable="correspondent__name"
|
||||||
title="Sort by correspondent" i18n-title
|
title="Sort by correspondent" i18n-title
|
||||||
@ -150,22 +178,28 @@
|
|||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
i18n>Correspondent</th>
|
i18n>Correspondent</th>
|
||||||
}
|
}
|
||||||
<th
|
@if (activeDisplayFields.includes(DisplayField.TITLE)) {
|
||||||
pngxSortable="title"
|
<th
|
||||||
title="Sort by title" i18n-title
|
pngxSortable="title"
|
||||||
class="w-40"
|
title="Sort by title" i18n-title
|
||||||
[currentSortField]="list.sortField"
|
[currentSortField]="list.sortField"
|
||||||
[currentSortReverse]="list.sortReverse"
|
[currentSortReverse]="list.sortReverse"
|
||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
i18n>Title</th>
|
i18n>Title</th>
|
||||||
<th class="d-none d-xl-table-cell"
|
}
|
||||||
pngxSortable="owner"
|
@if (activeDisplayFields.includes(DisplayField.TAGS) && !activeDisplayFields.includes(DisplayField.TITLE)) {
|
||||||
title="Sort by owner" i18n-title
|
<th i18n>Tags</th>
|
||||||
[currentSortField]="list.sortField"
|
}
|
||||||
[currentSortReverse]="list.sortReverse"
|
@if (activeDisplayFields.includes(DisplayField.OWNER) && permissionService.currentUserCan(PermissionAction.View, PermissionType.User)) {
|
||||||
(sort)="onSort($event)"
|
<th class="d-none d-xl-table-cell"
|
||||||
i18n>Owner</th>
|
pngxSortable="owner"
|
||||||
@if (notesEnabled) {
|
title="Sort by owner" i18n-title
|
||||||
|
[currentSortField]="list.sortField"
|
||||||
|
[currentSortReverse]="list.sortReverse"
|
||||||
|
(sort)="onSort($event)"
|
||||||
|
i18n>Owner</th>
|
||||||
|
}
|
||||||
|
@if (activeDisplayFields.includes(DisplayField.NOTES) && notesEnabled) {
|
||||||
<th class="d-none d-xl-table-cell"
|
<th class="d-none d-xl-table-cell"
|
||||||
pngxSortable="num_notes"
|
pngxSortable="num_notes"
|
||||||
title="Sort by notes" i18n-title
|
title="Sort by notes" i18n-title
|
||||||
@ -174,7 +208,7 @@
|
|||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
i18n>Notes</th>
|
i18n>Notes</th>
|
||||||
}
|
}
|
||||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
|
@if (activeDisplayFields.includes(DisplayField.DOCUMENT_TYPE) && permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
|
||||||
<th class="d-none d-xl-table-cell"
|
<th class="d-none d-xl-table-cell"
|
||||||
pngxSortable="document_type__name"
|
pngxSortable="document_type__name"
|
||||||
title="Sort by document type" i18n-title
|
title="Sort by document type" i18n-title
|
||||||
@ -183,7 +217,7 @@
|
|||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
i18n>Document type</th>
|
i18n>Document type</th>
|
||||||
}
|
}
|
||||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
|
@if (activeDisplayFields.includes(DisplayField.STORAGE_PATH) && permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
|
||||||
<th class="d-none d-xl-table-cell"
|
<th class="d-none d-xl-table-cell"
|
||||||
pngxSortable="storage_path__name"
|
pngxSortable="storage_path__name"
|
||||||
title="Sort by storage path" i18n-title
|
title="Sort by storage path" i18n-title
|
||||||
@ -192,20 +226,34 @@
|
|||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
i18n>Storage path</th>
|
i18n>Storage path</th>
|
||||||
}
|
}
|
||||||
<th
|
@if (activeDisplayFields.includes(DisplayField.CREATED)) {
|
||||||
pngxSortable="created"
|
<th
|
||||||
title="Sort by created date" i18n-title
|
pngxSortable="created"
|
||||||
[currentSortField]="list.sortField"
|
title="Sort by created date" i18n-title
|
||||||
[currentSortReverse]="list.sortReverse"
|
[currentSortField]="list.sortField"
|
||||||
(sort)="onSort($event)"
|
[currentSortReverse]="list.sortReverse"
|
||||||
i18n>Created</th>
|
(sort)="onSort($event)"
|
||||||
<th class="d-none d-xl-table-cell"
|
i18n>Created</th>
|
||||||
pngxSortable="added"
|
}
|
||||||
title="Sort by added date" i18n-title
|
@if (activeDisplayFields.includes(DisplayField.ADDED)) {
|
||||||
[currentSortField]="list.sortField"
|
<th
|
||||||
[currentSortReverse]="list.sortReverse"
|
pngxSortable="added"
|
||||||
(sort)="onSort($event)"
|
title="Sort by added date" i18n-title
|
||||||
i18n>Added</th>
|
[currentSortField]="list.sortField"
|
||||||
|
[currentSortReverse]="list.sortReverse"
|
||||||
|
(sort)="onSort($event)"
|
||||||
|
i18n>Added</th>
|
||||||
|
}
|
||||||
|
@if (activeDisplayFields.includes(DisplayField.SHARED)) {
|
||||||
|
<th i18n>
|
||||||
|
Shared
|
||||||
|
</th>
|
||||||
|
}
|
||||||
|
@for (field of activeDisplayCustomFields; track field) {
|
||||||
|
<th>
|
||||||
|
{{getDisplayCustomFieldTitle(field)}}
|
||||||
|
</th>
|
||||||
|
}
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (d of list.documents; track trackByDocumentId($index, d)) {
|
@for (d of list.documents; track trackByDocumentId($index, d)) {
|
||||||
@ -216,26 +264,36 @@
|
|||||||
<label class="form-check-label" for="docCheck{{d.id}}"></label>
|
<label class="form-check-label" for="docCheck{{d.id}}"></label>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="d-none d-lg-table-cell">
|
@if (activeDisplayFields.includes(DisplayField.ASN)) {
|
||||||
{{d.archive_serial_number}}
|
<td class="d-none d-xl-table-cell">
|
||||||
</td>
|
{{d.archive_serial_number}}
|
||||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
|
</td>
|
||||||
<td class="d-none d-md-table-cell">
|
}
|
||||||
|
@if (activeDisplayFields.includes(DisplayField.CORRESPONDENT) && permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
|
||||||
|
<td class="d-none d-xl-table-cell">
|
||||||
@if (d.correspondent) {
|
@if (d.correspondent) {
|
||||||
<a (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent" i18n-title>{{(d.correspondent$ | async)?.name}}</a>
|
<a (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent" i18n-title>{{(d.correspondent$ | async)?.name}}</a>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
}
|
}
|
||||||
<td>
|
@if (activeDisplayFields.includes(DisplayField.TITLE) || activeDisplayFields.includes(DisplayField.TAGS)) {
|
||||||
<a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
|
<td>
|
||||||
@for (t of d.tags$ | async; track t) {
|
@if (activeDisplayFields.includes(DisplayField.TITLE)) {
|
||||||
<pngx-tag [tag]="t" class="ms-1" clickable="true" linkTitle="Filter by tag" i18n-linkTitle (click)="clickTag(t.id);$event.stopPropagation()"></pngx-tag>
|
<a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
|
||||||
}
|
}
|
||||||
</td>
|
@if (activeDisplayFields.includes(DisplayField.TAGS)) {
|
||||||
<td>
|
@for (t of d.tags$ | async; track t) {
|
||||||
{{d.owner | username}}
|
<pngx-tag [tag]="t" class="ms-1" clickable="true" linkTitle="Filter by tag" i18n-linkTitle (click)="clickTag(t.id);$event.stopPropagation()"></pngx-tag>
|
||||||
</td>
|
}
|
||||||
@if (notesEnabled) {
|
}
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
@if (activeDisplayFields.includes(DisplayField.OWNER) && permissionService.currentUserCan(PermissionAction.View, PermissionType.User)) {
|
||||||
|
<td>
|
||||||
|
{{d.owner | username}}
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
@if (activeDisplayFields.includes(DisplayField.NOTES) && notesEnabled) {
|
||||||
<td class="d-none d-xl-table-cell">
|
<td class="d-none d-xl-table-cell">
|
||||||
@if (d.notes.length) {
|
@if (d.notes.length) {
|
||||||
<a routerLink="/documents/{{d.id}}/notes" class="btn btn-sm p-0">
|
<a routerLink="/documents/{{d.id}}/notes" class="btn btn-sm p-0">
|
||||||
@ -246,35 +304,59 @@
|
|||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
}
|
}
|
||||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
|
@if (activeDisplayFields.includes(DisplayField.DOCUMENT_TYPE) && permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
|
||||||
<td class="d-none d-xl-table-cell">
|
<td class="d-none d-xl-table-cell">
|
||||||
@if (d.document_type) {
|
@if (d.document_type) {
|
||||||
<a (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type" i18n-title>{{(d.document_type$ | async)?.name}}</a>
|
<a (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type" i18n-title>{{(d.document_type$ | async)?.name}}</a>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
}
|
}
|
||||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
|
@if (activeDisplayFields.includes(DisplayField.STORAGE_PATH) && permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
|
||||||
<td class="d-none d-xl-table-cell">
|
<td class="d-none d-xl-table-cell">
|
||||||
@if (d.storage_path) {
|
@if (d.storage_path) {
|
||||||
<a (click)="clickStoragePath(d.storage_path);$event.stopPropagation()" title="Filter by storage path" i18n-title>{{(d.storage_path$ | async)?.name}}</a>
|
<a (click)="clickStoragePath(d.storage_path);$event.stopPropagation()" title="Filter by storage path" i18n-title>{{(d.storage_path$ | async)?.name}}</a>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
}
|
}
|
||||||
<td>
|
@if (activeDisplayFields.includes(DisplayField.CREATED)) {
|
||||||
{{d.created_date | customDate}}
|
<td>
|
||||||
</td>
|
{{d.created_date | customDate}}
|
||||||
<td class="d-none d-xl-table-cell">
|
</td>
|
||||||
{{d.added | customDate}}
|
}
|
||||||
</td>
|
@if (activeDisplayFields.includes(DisplayField.ADDED)) {
|
||||||
|
<td>
|
||||||
|
{{d.added | customDate}}
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
@if (activeDisplayFields.includes(DisplayField.SHARED)) {
|
||||||
|
<td>
|
||||||
|
@if (d.is_shared_by_requester) { <ng-container i18n>Yes</ng-container> } @else { <ng-container i18n>No</ng-container> }
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
@for (field of activeDisplayCustomFields; track field) {
|
||||||
|
<td class="d-none d-xl-table-cell">
|
||||||
|
<pngx-custom-field-display [document]="d" [fieldDisplayKey]="field"></pngx-custom-field-display>
|
||||||
|
</td>
|
||||||
|
}
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
}
|
}
|
||||||
@if (displayMode === 'smallCards') {
|
@if (list.displayMode === DisplayMode.SMALL_CARDS) {
|
||||||
<div class="row row-cols-paperless-cards">
|
<div class="row row-cols-paperless-cards">
|
||||||
@for (d of list.documents; track trackByDocumentId($index, d)) {
|
@for (d of list.documents; track trackByDocumentId($index, d)) {
|
||||||
<pngx-document-card-small class="p-0" [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" (dblClickDocument)="openDocumentDetail(d)" [document]="d" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickStoragePath)="clickStoragePath($event)" (clickDocumentType)="clickDocumentType($event)"></pngx-document-card-small>
|
<pngx-document-card-small class="p-0"
|
||||||
|
[selected]="list.isSelected(d)"
|
||||||
|
(toggleSelected)="toggleSelected(d, $event)"
|
||||||
|
(dblClickDocument)="openDocumentDetail(d)"
|
||||||
|
[document]="d"
|
||||||
|
(clickTag)="clickTag($event)"
|
||||||
|
[displayFields]="activeDisplayFields"
|
||||||
|
(clickCorrespondent)="clickCorrespondent($event)"
|
||||||
|
(clickStoragePath)="clickStoragePath($event)"
|
||||||
|
(clickDocumentType)="clickDocumentType($event)">
|
||||||
|
</pngx-document-card-small>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -10,10 +10,6 @@ th {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
th.w-40 {
|
|
||||||
width: 40%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-row-selected {
|
.table-row-selected {
|
||||||
background-color: var(--pngx-primary-faded);
|
background-color: var(--pngx-primary-faded);
|
||||||
}
|
}
|
||||||
@ -84,3 +80,7 @@ $paperless-card-breakpoints: (
|
|||||||
a {
|
a {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pngx-page-header .dropdown-menu {
|
||||||
|
--bs-dropdown-min-width: 12em;
|
||||||
|
}
|
||||||
|
@ -47,12 +47,13 @@ import { DocumentCardSmallComponent } from './document-card-small/document-card-
|
|||||||
import { DocumentCardLargeComponent } from './document-card-large/document-card-large.component'
|
import { DocumentCardLargeComponent } from './document-card-large/document-card-large.component'
|
||||||
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
||||||
import { UsernamePipe } from 'src/app/pipes/username.pipe'
|
import { UsernamePipe } from 'src/app/pipes/username.pipe'
|
||||||
import { Document } from 'src/app/data/document'
|
|
||||||
import {
|
import {
|
||||||
DOCUMENT_SORT_FIELDS,
|
DEFAULT_DISPLAY_FIELDS,
|
||||||
DOCUMENT_SORT_FIELDS_FULLTEXT,
|
DisplayField,
|
||||||
DocumentService,
|
DisplayMode,
|
||||||
} from 'src/app/services/rest/document.service'
|
Document,
|
||||||
|
} from 'src/app/data/document'
|
||||||
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
|
||||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||||
import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component'
|
import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component'
|
||||||
@ -169,17 +170,6 @@ describe('DocumentListComponent', () => {
|
|||||||
component = fixture.componentInstance
|
component = fixture.componentInstance
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should load display mode from local storage', () => {
|
|
||||||
window.localStorage.setItem('document-list:displayMode', 'largeCards')
|
|
||||||
fixture.detectChanges()
|
|
||||||
expect(component.displayMode).toEqual('largeCards')
|
|
||||||
component.displayMode = 'smallCards'
|
|
||||||
component.saveDisplayMode()
|
|
||||||
expect(window.localStorage.getItem('document-list:displayMode')).toEqual(
|
|
||||||
'smallCards'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should reload on new document consumed', () => {
|
it('should reload on new document consumed', () => {
|
||||||
const reloadSpy = jest.spyOn(documentListService, 'reload')
|
const reloadSpy = jest.spyOn(documentListService, 'reload')
|
||||||
const fileStatusSubject = new Subject<FileStatus>()
|
const fileStatusSubject = new Subject<FileStatus>()
|
||||||
@ -199,7 +189,7 @@ describe('DocumentListComponent', () => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.getSortFields()).toEqual(DOCUMENT_SORT_FIELDS)
|
expect(component.getSortFields()).toEqual(documentListService.sortFields)
|
||||||
|
|
||||||
documentListService.filterRules = [
|
documentListService.filterRules = [
|
||||||
{
|
{
|
||||||
@ -208,7 +198,9 @@ describe('DocumentListComponent', () => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.getSortFields()).toEqual(DOCUMENT_SORT_FIELDS_FULLTEXT)
|
expect(component.getSortFields()).toEqual(
|
||||||
|
documentListService.sortFieldsFullText
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should determine if filtered, support reset', () => {
|
it('should determine if filtered, support reset', () => {
|
||||||
@ -297,18 +289,18 @@ describe('DocumentListComponent', () => {
|
|||||||
const displayModeButtons = fixture.debugElement.queryAll(
|
const displayModeButtons = fixture.debugElement.queryAll(
|
||||||
By.css('input[type="radio"]')
|
By.css('input[type="radio"]')
|
||||||
)
|
)
|
||||||
expect(component.displayMode).toEqual('smallCards')
|
expect(component.list.displayMode).toEqual('smallCards')
|
||||||
|
|
||||||
displayModeButtons[0].nativeElement.checked = true
|
displayModeButtons[0].nativeElement.checked = true
|
||||||
displayModeButtons[0].triggerEventHandler('change')
|
displayModeButtons[0].triggerEventHandler('change')
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.displayMode).toEqual('details')
|
expect(component.list.displayMode).toEqual('table')
|
||||||
expect(fixture.debugElement.queryAll(By.css('tr'))).toHaveLength(3)
|
expect(fixture.debugElement.queryAll(By.css('tr'))).toHaveLength(3)
|
||||||
|
|
||||||
displayModeButtons[1].nativeElement.checked = true
|
displayModeButtons[1].nativeElement.checked = true
|
||||||
displayModeButtons[1].triggerEventHandler('change')
|
displayModeButtons[1].triggerEventHandler('change')
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.displayMode).toEqual('smallCards')
|
expect(component.list.displayMode).toEqual('smallCards')
|
||||||
expect(
|
expect(
|
||||||
fixture.debugElement.queryAll(By.directive(DocumentCardSmallComponent))
|
fixture.debugElement.queryAll(By.directive(DocumentCardSmallComponent))
|
||||||
).toHaveLength(3)
|
).toHaveLength(3)
|
||||||
@ -316,7 +308,7 @@ describe('DocumentListComponent', () => {
|
|||||||
displayModeButtons[2].nativeElement.checked = true
|
displayModeButtons[2].nativeElement.checked = true
|
||||||
displayModeButtons[2].triggerEventHandler('change')
|
displayModeButtons[2].triggerEventHandler('change')
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.displayMode).toEqual('largeCards')
|
expect(component.list.displayMode).toEqual('largeCards')
|
||||||
expect(
|
expect(
|
||||||
fixture.debugElement.queryAll(By.directive(DocumentCardLargeComponent))
|
fixture.debugElement.queryAll(By.directive(DocumentCardLargeComponent))
|
||||||
).toHaveLength(3)
|
).toHaveLength(3)
|
||||||
@ -327,7 +319,7 @@ describe('DocumentListComponent', () => {
|
|||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
const sortDropdown = fixture.debugElement.queryAll(
|
const sortDropdown = fixture.debugElement.queryAll(
|
||||||
By.directive(NgbDropdown)
|
By.directive(NgbDropdown)
|
||||||
)[1]
|
)[2]
|
||||||
const asnSortFieldButton = sortDropdown.query(By.directive(NgbDropdownItem))
|
const asnSortFieldButton = sortDropdown.query(By.directive(NgbDropdownItem))
|
||||||
|
|
||||||
asnSortFieldButton.triggerEventHandler('click')
|
asnSortFieldButton.triggerEventHandler('click')
|
||||||
@ -337,6 +329,7 @@ describe('DocumentListComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should support setting sort field by table head', () => {
|
it('should support setting sort field by table head', () => {
|
||||||
|
component.activeDisplayFields = [DisplayField.ASN]
|
||||||
jest.spyOn(documentListService, 'documents', 'get').mockReturnValue(docs)
|
jest.spyOn(documentListService, 'documents', 'get').mockReturnValue(docs)
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(documentListService.sortField).toEqual('created')
|
expect(documentListService.sortField).toEqual('created')
|
||||||
@ -347,7 +340,7 @@ describe('DocumentListComponent', () => {
|
|||||||
detailsDisplayModeButton.nativeElement.checked = true
|
detailsDisplayModeButton.nativeElement.checked = true
|
||||||
detailsDisplayModeButton.triggerEventHandler('change')
|
detailsDisplayModeButton.triggerEventHandler('change')
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.displayMode).toEqual('details')
|
expect(component.list.displayMode).toEqual(DisplayMode.TABLE)
|
||||||
|
|
||||||
const sortTh = fixture.debugElement.query(By.directive(SortableDirective))
|
const sortTh = fixture.debugElement.query(By.directive(SortableDirective))
|
||||||
sortTh.triggerEventHandler('click')
|
sortTh.triggerEventHandler('click')
|
||||||
@ -430,6 +423,8 @@ describe('DocumentListComponent', () => {
|
|||||||
value: '20',
|
value: '20',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
display_mode: DisplayMode.SMALL_CARDS,
|
||||||
|
display_fields: [DisplayField.TITLE],
|
||||||
}
|
}
|
||||||
jest.spyOn(savedViewService, 'getCached').mockReturnValue(of(view))
|
jest.spyOn(savedViewService, 'getCached').mockReturnValue(of(view))
|
||||||
const queryParams = { view: view.id.toString() }
|
const queryParams = { view: view.id.toString() }
|
||||||
@ -546,6 +541,42 @@ describe('DocumentListComponent', () => {
|
|||||||
expect(openModal.componentInstance.error).toEqual({ filter_rules: ['11'] })
|
expect(openModal.componentInstance.error).toEqual({ filter_rules: ['11'] })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should detect saved view changes', () => {
|
||||||
|
const view: SavedView = {
|
||||||
|
id: 10,
|
||||||
|
name: 'Saved View 10',
|
||||||
|
sort_field: 'added',
|
||||||
|
sort_reverse: true,
|
||||||
|
filter_rules: [
|
||||||
|
{
|
||||||
|
rule_type: FILTER_HAS_TAGS_ANY,
|
||||||
|
value: '20',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
page_size: 5,
|
||||||
|
display_mode: DisplayMode.SMALL_CARDS,
|
||||||
|
display_fields: [DisplayField.TITLE],
|
||||||
|
}
|
||||||
|
jest.spyOn(savedViewService, 'getCached').mockReturnValue(of(view))
|
||||||
|
const queryParams = { view: view.id.toString() }
|
||||||
|
jest
|
||||||
|
.spyOn(activatedRoute, 'queryParamMap', 'get')
|
||||||
|
.mockReturnValue(of(convertToParamMap(queryParams)))
|
||||||
|
activatedRoute.snapshot.queryParams = queryParams
|
||||||
|
router.routerState.snapshot.url = '/view/10/'
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(documentListService.activeSavedViewId).toEqual(10)
|
||||||
|
|
||||||
|
component.list.displayFields = [DisplayField.ASN]
|
||||||
|
expect(component.savedViewIsModified).toBeTruthy()
|
||||||
|
component.list.displayFields = [DisplayField.TITLE]
|
||||||
|
expect(component.savedViewIsModified).toBeFalsy()
|
||||||
|
component.list.displayMode = DisplayMode.TABLE
|
||||||
|
expect(component.savedViewIsModified).toBeTruthy()
|
||||||
|
component.list.displayMode = DisplayMode.SMALL_CARDS
|
||||||
|
expect(component.savedViewIsModified).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
it('should navigate to a document', () => {
|
it('should navigate to a document', () => {
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
const routerSpy = jest.spyOn(router, 'navigate')
|
const routerSpy = jest.spyOn(router, 'navigate')
|
||||||
@ -558,7 +589,8 @@ describe('DocumentListComponent', () => {
|
|||||||
jest.spyOn(documentListService, 'documents', 'get').mockReturnValue(docs)
|
jest.spyOn(documentListService, 'documents', 'get').mockReturnValue(docs)
|
||||||
expect(documentListService.sortField).toEqual('created')
|
expect(documentListService.sortField).toEqual('created')
|
||||||
|
|
||||||
component.displayMode = 'details'
|
component.list.displayMode = DisplayMode.TABLE
|
||||||
|
component.list.displayFields = DEFAULT_DISPLAY_FIELDS.map((f) => f.id)
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
@ -578,7 +610,7 @@ describe('DocumentListComponent', () => {
|
|||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(
|
expect(
|
||||||
fixture.debugElement.queryAll(By.directive(SortableDirective))
|
fixture.debugElement.queryAll(By.directive(SortableDirective))
|
||||||
).toHaveLength(5)
|
).toHaveLength(4)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should support toggle on document objects', () => {
|
it('should support toggle on document objects', () => {
|
||||||
@ -598,4 +630,28 @@ describe('DocumentListComponent', () => {
|
|||||||
{ rule_type: FILTER_FULLTEXT_MORELIKE, value: '99' },
|
{ rule_type: FILTER_FULLTEXT_MORELIKE, value: '99' },
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should support toggling display fields', () => {
|
||||||
|
fixture.detectChanges()
|
||||||
|
component.activeDisplayFields = [DisplayField.ASN]
|
||||||
|
component.toggleDisplayField(DisplayField.TITLE)
|
||||||
|
expect(component.activeDisplayFields).toEqual([
|
||||||
|
DisplayField.ASN,
|
||||||
|
DisplayField.TITLE,
|
||||||
|
])
|
||||||
|
component.toggleDisplayField(DisplayField.ASN)
|
||||||
|
expect(component.activeDisplayFields).toEqual([DisplayField.TITLE])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should get custom field title', () => {
|
||||||
|
fixture.detectChanges()
|
||||||
|
jest
|
||||||
|
.spyOn(settingsService, 'allDisplayFields', 'get')
|
||||||
|
.mockReturnValue([
|
||||||
|
{ id: 'custom_field_1' as any, name: 'Custom Field 1' },
|
||||||
|
])
|
||||||
|
expect(component.getDisplayCustomFieldTitle('custom_field_1')).toEqual(
|
||||||
|
'Custom Field 1'
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -15,7 +15,7 @@ import {
|
|||||||
isFullTextFilterRule,
|
isFullTextFilterRule,
|
||||||
} from 'src/app/utils/filter-rules'
|
} from 'src/app/utils/filter-rules'
|
||||||
import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type'
|
import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type'
|
||||||
import { Document } from 'src/app/data/document'
|
import { DisplayField, DisplayMode, Document } from 'src/app/data/document'
|
||||||
import { SavedView } from 'src/app/data/saved-view'
|
import { SavedView } from 'src/app/data/saved-view'
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
import {
|
import {
|
||||||
@ -25,10 +25,6 @@ import {
|
|||||||
import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
|
import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
|
||||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||||
import {
|
|
||||||
DOCUMENT_SORT_FIELDS,
|
|
||||||
DOCUMENT_SORT_FIELDS_FULLTEXT,
|
|
||||||
} from 'src/app/services/rest/document.service'
|
|
||||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
@ -46,6 +42,9 @@ export class DocumentListComponent
|
|||||||
extends ComponentWithPermissions
|
extends ComponentWithPermissions
|
||||||
implements OnInit, OnDestroy
|
implements OnInit, OnDestroy
|
||||||
{
|
{
|
||||||
|
DisplayField = DisplayField
|
||||||
|
DisplayMode = DisplayMode
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public list: DocumentListViewService,
|
public list: DocumentListViewService,
|
||||||
public savedViewService: SavedViewService,
|
public savedViewService: SavedViewService,
|
||||||
@ -55,7 +54,7 @@ export class DocumentListComponent
|
|||||||
private modalService: NgbModal,
|
private modalService: NgbModal,
|
||||||
private consumerStatusService: ConsumerStatusService,
|
private consumerStatusService: ConsumerStatusService,
|
||||||
public openDocumentsService: OpenDocumentsService,
|
public openDocumentsService: OpenDocumentsService,
|
||||||
private settingsService: SettingsService,
|
public settingsService: SettingsService,
|
||||||
public permissionService: PermissionsService
|
public permissionService: PermissionsService
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
@ -66,7 +65,25 @@ export class DocumentListComponent
|
|||||||
|
|
||||||
@ViewChildren(SortableDirective) headers: QueryList<SortableDirective>
|
@ViewChildren(SortableDirective) headers: QueryList<SortableDirective>
|
||||||
|
|
||||||
displayMode = 'smallCards' // largeCards, smallCards, details
|
get activeDisplayFields(): DisplayField[] {
|
||||||
|
return this.list.displayFields
|
||||||
|
}
|
||||||
|
|
||||||
|
set activeDisplayFields(fields: DisplayField[]) {
|
||||||
|
this.list.displayFields = fields
|
||||||
|
this.updateDisplayCustomFields()
|
||||||
|
}
|
||||||
|
activeDisplayCustomFields: Set<string> = new Set()
|
||||||
|
|
||||||
|
public updateDisplayCustomFields() {
|
||||||
|
this.activeDisplayCustomFields = new Set(
|
||||||
|
Array.from(this.activeDisplayFields).filter(
|
||||||
|
(field) =>
|
||||||
|
typeof field === 'string' &&
|
||||||
|
field.startsWith(DisplayField.CUSTOM_FIELD)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
unmodifiedFilterRules: FilterRule[] = []
|
unmodifiedFilterRules: FilterRule[] = []
|
||||||
private unmodifiedSavedView: SavedView
|
private unmodifiedSavedView: SavedView
|
||||||
@ -79,6 +96,16 @@ export class DocumentListComponent
|
|||||||
return (
|
return (
|
||||||
this.unmodifiedSavedView.sort_field !== this.list.sortField ||
|
this.unmodifiedSavedView.sort_field !== this.list.sortField ||
|
||||||
this.unmodifiedSavedView.sort_reverse !== this.list.sortReverse ||
|
this.unmodifiedSavedView.sort_reverse !== this.list.sortReverse ||
|
||||||
|
(this.unmodifiedSavedView.page_size &&
|
||||||
|
this.unmodifiedSavedView.page_size !== this.list.pageSize) ||
|
||||||
|
(this.unmodifiedSavedView.display_mode &&
|
||||||
|
this.unmodifiedSavedView.display_mode !== this.list.displayMode) ||
|
||||||
|
// if the saved view has no display mode, we assume it's small cards
|
||||||
|
(!this.unmodifiedSavedView.display_mode &&
|
||||||
|
this.list.displayMode !== DisplayMode.SMALL_CARDS) ||
|
||||||
|
(this.unmodifiedSavedView.display_fields &&
|
||||||
|
this.unmodifiedSavedView.display_fields.join(',') !==
|
||||||
|
this.activeDisplayFields.join(',')) ||
|
||||||
filterRulesDiffer(
|
filterRulesDiffer(
|
||||||
this.unmodifiedSavedView.filter_rules,
|
this.unmodifiedSavedView.filter_rules,
|
||||||
this.list.filterRules
|
this.list.filterRules
|
||||||
@ -103,8 +130,8 @@ export class DocumentListComponent
|
|||||||
|
|
||||||
getSortFields() {
|
getSortFields() {
|
||||||
return isFullTextFilterRule(this.list.filterRules)
|
return isFullTextFilterRule(this.list.filterRules)
|
||||||
? DOCUMENT_SORT_FIELDS_FULLTEXT
|
? this.list.sortFieldsFullText
|
||||||
: DOCUMENT_SORT_FIELDS
|
: this.list.sortFields
|
||||||
}
|
}
|
||||||
|
|
||||||
set listSortReverse(reverse: boolean) {
|
set listSortReverse(reverse: boolean) {
|
||||||
@ -115,10 +142,6 @@ export class DocumentListComponent
|
|||||||
return this.list.sortReverse
|
return this.list.sortReverse
|
||||||
}
|
}
|
||||||
|
|
||||||
setSortField(field: string) {
|
|
||||||
this.list.sortField = field
|
|
||||||
}
|
|
||||||
|
|
||||||
onSort(event: SortEvent) {
|
onSort(event: SortEvent) {
|
||||||
this.list.setSort(event.column, event.reverse)
|
this.list.setSort(event.column, event.reverse)
|
||||||
}
|
}
|
||||||
@ -127,15 +150,23 @@ export class DocumentListComponent
|
|||||||
return this.list.selected.size > 0
|
return this.list.selected.size > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
saveDisplayMode() {
|
toggleDisplayField(field: DisplayField) {
|
||||||
localStorage.setItem('document-list:displayMode', this.displayMode)
|
if (this.activeDisplayFields.includes(field)) {
|
||||||
|
this.activeDisplayFields = this.activeDisplayFields.filter(
|
||||||
|
(f) => f !== field
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.activeDisplayFields = [...this.activeDisplayFields, field]
|
||||||
|
}
|
||||||
|
this.updateDisplayCustomFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
public getDisplayCustomFieldTitle(field: string) {
|
||||||
|
return this.settingsService.allDisplayFields.find((f) => f.id === field)
|
||||||
|
?.name
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (localStorage.getItem('document-list:displayMode') != null) {
|
|
||||||
this.displayMode = localStorage.getItem('document-list:displayMode')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.consumerStatusService
|
this.consumerStatusService
|
||||||
.onDocumentConsumptionFinished()
|
.onDocumentConsumptionFinished()
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
@ -199,6 +230,8 @@ export class DocumentListComponent
|
|||||||
filter_rules: this.list.filterRules,
|
filter_rules: this.list.filterRules,
|
||||||
sort_field: this.list.sortField,
|
sort_field: this.list.sortField,
|
||||||
sort_reverse: this.list.sortReverse,
|
sort_reverse: this.list.sortReverse,
|
||||||
|
display_mode: this.list.displayMode,
|
||||||
|
display_fields: this.activeDisplayFields,
|
||||||
}
|
}
|
||||||
this.savedViewService
|
this.savedViewService
|
||||||
.patch(savedView)
|
.patch(savedView)
|
||||||
@ -238,6 +271,8 @@ export class DocumentListComponent
|
|||||||
filter_rules: this.list.filterRules,
|
filter_rules: this.list.filterRules,
|
||||||
sort_reverse: this.list.sortReverse,
|
sort_reverse: this.list.sortReverse,
|
||||||
sort_field: this.list.sortField,
|
sort_field: this.list.sortField,
|
||||||
|
display_mode: this.list.displayMode,
|
||||||
|
display_fields: this.activeDisplayFields,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.savedViewService
|
this.savedViewService
|
||||||
|
@ -7,6 +7,102 @@ import { ObjectWithPermissions } from './object-with-permissions'
|
|||||||
import { DocumentNote } from './document-note'
|
import { DocumentNote } from './document-note'
|
||||||
import { CustomFieldInstance } from './custom-field-instance'
|
import { CustomFieldInstance } from './custom-field-instance'
|
||||||
|
|
||||||
|
export enum DisplayMode {
|
||||||
|
TABLE = 'table',
|
||||||
|
SMALL_CARDS = 'smallCards',
|
||||||
|
LARGE_CARDS = 'largeCards',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DisplayField {
|
||||||
|
TITLE = 'title',
|
||||||
|
CREATED = 'created',
|
||||||
|
ADDED = 'added',
|
||||||
|
TAGS = 'tag',
|
||||||
|
CORRESPONDENT = 'correspondent',
|
||||||
|
DOCUMENT_TYPE = 'documenttype',
|
||||||
|
STORAGE_PATH = 'storagepath',
|
||||||
|
CUSTOM_FIELD = 'custom_field_',
|
||||||
|
NOTES = 'note',
|
||||||
|
OWNER = 'owner',
|
||||||
|
SHARED = 'shared',
|
||||||
|
ASN = 'asn',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_DISPLAY_FIELDS = [
|
||||||
|
{
|
||||||
|
id: DisplayField.TITLE,
|
||||||
|
name: $localize`Title`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: DisplayField.CREATED,
|
||||||
|
name: $localize`Created`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: DisplayField.ADDED,
|
||||||
|
name: $localize`Added`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: DisplayField.TAGS,
|
||||||
|
name: $localize`Tags`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: DisplayField.CORRESPONDENT,
|
||||||
|
name: $localize`Correspondent`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: DisplayField.DOCUMENT_TYPE,
|
||||||
|
name: $localize`Document type`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: DisplayField.STORAGE_PATH,
|
||||||
|
name: $localize`Storage path`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: DisplayField.NOTES,
|
||||||
|
name: $localize`Notes`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: DisplayField.OWNER,
|
||||||
|
name: $localize`Owner`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: DisplayField.SHARED,
|
||||||
|
name: $localize`Shared`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: DisplayField.ASN,
|
||||||
|
name: $localize`ASN`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const DEFAULT_DASHBOARD_VIEW_PAGE_SIZE = 10
|
||||||
|
|
||||||
|
export const DEFAULT_DASHBOARD_DISPLAY_FIELDS = [
|
||||||
|
DisplayField.CREATED,
|
||||||
|
DisplayField.TITLE,
|
||||||
|
DisplayField.TAGS,
|
||||||
|
DisplayField.CORRESPONDENT,
|
||||||
|
]
|
||||||
|
|
||||||
|
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 = [
|
||||||
|
{
|
||||||
|
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 SearchHit {
|
export interface SearchHit {
|
||||||
score?: number
|
score?: number
|
||||||
rank?: number
|
rank?: number
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { DisplayMode, DisplayField } from './document'
|
||||||
import { FilterRule } from './filter-rule'
|
import { FilterRule } from './filter-rule'
|
||||||
import { ObjectWithPermissions } from './object-with-permissions'
|
import { ObjectWithPermissions } from './object-with-permissions'
|
||||||
|
|
||||||
@ -13,4 +14,10 @@ export interface SavedView extends ObjectWithPermissions {
|
|||||||
sort_reverse: boolean
|
sort_reverse: boolean
|
||||||
|
|
||||||
filter_rules: FilterRule[]
|
filter_rules: FilterRule[]
|
||||||
|
|
||||||
|
page_size?: number
|
||||||
|
|
||||||
|
display_mode?: DisplayMode
|
||||||
|
|
||||||
|
display_fields?: DisplayField[]
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,8 @@ describe('ConsumerStatusService', () => {
|
|||||||
let httpTestingController: HttpTestingController
|
let httpTestingController: HttpTestingController
|
||||||
let consumerStatusService: ConsumerStatusService
|
let consumerStatusService: ConsumerStatusService
|
||||||
let documentService: DocumentService
|
let documentService: DocumentService
|
||||||
|
let settingsService: SettingsService
|
||||||
|
|
||||||
const server = new WS(
|
const server = new WS(
|
||||||
`${environment.webSocketProtocol}//${environment.webSocketHost}${environment.webSocketBaseUrl}status/`,
|
`${environment.webSocketProtocol}//${environment.webSocketHost}${environment.webSocketBaseUrl}status/`,
|
||||||
{ jsonProtocol: true }
|
{ jsonProtocol: true }
|
||||||
@ -25,25 +27,17 @@ describe('ConsumerStatusService', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [ConsumerStatusService, DocumentService, SettingsService],
|
||||||
ConsumerStatusService,
|
|
||||||
DocumentService,
|
|
||||||
SettingsService,
|
|
||||||
{
|
|
||||||
provide: SettingsService,
|
|
||||||
useValue: {
|
|
||||||
currentUser: {
|
|
||||||
id: 1,
|
|
||||||
username: 'testuser',
|
|
||||||
is_superuser: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
imports: [HttpClientTestingModule],
|
imports: [HttpClientTestingModule],
|
||||||
})
|
})
|
||||||
|
|
||||||
httpTestingController = TestBed.inject(HttpTestingController)
|
httpTestingController = TestBed.inject(HttpTestingController)
|
||||||
|
settingsService = TestBed.inject(SettingsService)
|
||||||
|
settingsService.currentUser = {
|
||||||
|
id: 1,
|
||||||
|
username: 'testuser',
|
||||||
|
is_superuser: false,
|
||||||
|
}
|
||||||
consumerStatusService = TestBed.inject(ConsumerStatusService)
|
consumerStatusService = TestBed.inject(ConsumerStatusService)
|
||||||
documentService = TestBed.inject(DocumentService)
|
documentService = TestBed.inject(DocumentService)
|
||||||
})
|
})
|
||||||
|
@ -19,6 +19,11 @@ import { routes } from 'src/app/app-routing.module'
|
|||||||
import { PermissionsGuard } from '../guards/permissions.guard'
|
import { PermissionsGuard } from '../guards/permissions.guard'
|
||||||
import { SettingsService } from './settings.service'
|
import { SettingsService } from './settings.service'
|
||||||
import { SETTINGS_KEYS } from '../data/ui-settings'
|
import { SETTINGS_KEYS } from '../data/ui-settings'
|
||||||
|
import {
|
||||||
|
DisplayMode,
|
||||||
|
DisplayField,
|
||||||
|
DEFAULT_DISPLAY_FIELDS,
|
||||||
|
} from '../data/document'
|
||||||
|
|
||||||
const documents = [
|
const documents = [
|
||||||
{
|
{
|
||||||
@ -213,7 +218,7 @@ describe('DocumentListViewService', () => {
|
|||||||
documentListViewService.loadFromQueryParams(convertToParamMap(params))
|
documentListViewService.loadFromQueryParams(convertToParamMap(params))
|
||||||
const req = httpTestingController.expectOne(
|
const req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=${page}&page_size=${
|
`${environment.apiBaseUrl}documents/?page=${page}&page_size=${
|
||||||
documentListViewService.currentPageSize
|
documentListViewService.pageSize
|
||||||
}&ordering=${reverse ? '-' : ''}${sort}&truncate_content=true`
|
}&ordering=${reverse ? '-' : ''}${sort}&truncate_content=true`
|
||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('GET')
|
expect(req.request.method).toEqual('GET')
|
||||||
@ -231,7 +236,7 @@ describe('DocumentListViewService', () => {
|
|||||||
}
|
}
|
||||||
documentListViewService.loadFromQueryParams(convertToParamMap(params))
|
documentListViewService.loadFromQueryParams(convertToParamMap(params))
|
||||||
let req = httpTestingController.expectOne(
|
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(req.request.method).toEqual('GET')
|
||||||
expect(documentListViewService.filterRules).toEqual([
|
expect(documentListViewService.filterRules).toEqual([
|
||||||
@ -249,7 +254,7 @@ describe('DocumentListViewService', () => {
|
|||||||
it('should use filter rules to update query params', () => {
|
it('should use filter rules to update query params', () => {
|
||||||
documentListViewService.filterRules = filterRules
|
documentListViewService.filterRules = filterRules
|
||||||
const req = httpTestingController.expectOne(
|
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')
|
expect(req.request.method).toEqual('GET')
|
||||||
})
|
})
|
||||||
@ -257,7 +262,7 @@ describe('DocumentListViewService', () => {
|
|||||||
it('should support quick filter', () => {
|
it('should support quick filter', () => {
|
||||||
documentListViewService.quickFilter(filterRules)
|
documentListViewService.quickFilter(filterRules)
|
||||||
const req = httpTestingController.expectOne(
|
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')
|
expect(req.request.method).toEqual('GET')
|
||||||
})
|
})
|
||||||
@ -280,7 +285,7 @@ describe('DocumentListViewService', () => {
|
|||||||
convertToParamMap(params)
|
convertToParamMap(params)
|
||||||
)
|
)
|
||||||
let req = httpTestingController.expectOne(
|
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')
|
expect(req.request.method).toEqual('GET')
|
||||||
// reset the list
|
// reset the list
|
||||||
@ -305,8 +310,7 @@ describe('DocumentListViewService', () => {
|
|||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||||
)
|
)
|
||||||
expect(documentListViewService.currentPage).toEqual(1)
|
expect(documentListViewService.currentPage).toEqual(1)
|
||||||
documentListViewService.currentPageSize = 3
|
documentListViewService.pageSize = 3
|
||||||
documentListViewService.reload()
|
|
||||||
req = httpTestingController.expectOne(
|
req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=3&ordering=-created&truncate_content=true`
|
`${environment.apiBaseUrl}documents/?page=1&page_size=3&ordering=-created&truncate_content=true`
|
||||||
)
|
)
|
||||||
@ -362,7 +366,10 @@ describe('DocumentListViewService', () => {
|
|||||||
.spyOn(documentListViewService, 'documents', 'get')
|
.spyOn(documentListViewService, 'documents', 'get')
|
||||||
.mockReturnValue(documents)
|
.mockReturnValue(documents)
|
||||||
expect(documentListViewService.currentPage).toEqual(1)
|
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
|
jest
|
||||||
.spyOn(documentListViewService, 'getLastPage')
|
.spyOn(documentListViewService, 'getLastPage')
|
||||||
.mockReturnValue(Math.ceil(documents.length / 3))
|
.mockReturnValue(Math.ceil(documents.length / 3))
|
||||||
@ -410,7 +417,13 @@ describe('DocumentListViewService', () => {
|
|||||||
.spyOn(documentListViewService, 'documents', 'get')
|
.spyOn(documentListViewService, 'documents', 'get')
|
||||||
.mockReturnValue(documents)
|
.mockReturnValue(documents)
|
||||||
documentListViewService.currentPage = 2
|
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')
|
const reloadSpy = jest.spyOn(documentListViewService, 'reload')
|
||||||
documentListViewService.getPrevious(1).subscribe({
|
documentListViewService.getPrevious(1).subscribe({
|
||||||
next: () => {},
|
next: () => {},
|
||||||
@ -426,8 +439,7 @@ describe('DocumentListViewService', () => {
|
|||||||
|
|
||||||
it('should update page size from settings', () => {
|
it('should update page size from settings', () => {
|
||||||
settingsService.set(SETTINGS_KEYS.DOCUMENT_LIST_SIZE, 10)
|
settingsService.set(SETTINGS_KEYS.DOCUMENT_LIST_SIZE, 10)
|
||||||
documentListViewService.updatePageSize()
|
expect(documentListViewService.pageSize).toEqual(10)
|
||||||
expect(documentListViewService.currentPageSize).toEqual(10)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should support select a document', () => {
|
it('should support select a document', () => {
|
||||||
@ -459,8 +471,7 @@ describe('DocumentListViewService', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should support select page', () => {
|
it('should support select page', () => {
|
||||||
documentListViewService.currentPageSize = 3
|
documentListViewService.pageSize = 3
|
||||||
documentListViewService.reload()
|
|
||||||
const req = httpTestingController.expectOne(
|
const req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/?page=1&page_size=3&ordering=-created&truncate_content=true`
|
`${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`
|
`${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,
|
cloneFilterRules,
|
||||||
isFullTextFilterRule,
|
isFullTextFilterRule,
|
||||||
} from '../utils/filter-rules'
|
} 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 { SavedView } from '../data/saved-view'
|
||||||
import { SETTINGS_KEYS } from '../data/ui-settings'
|
import { SETTINGS_KEYS } from '../data/ui-settings'
|
||||||
import { DOCUMENT_LIST_SERVICE } from '../data/storage-keys'
|
import { DOCUMENT_LIST_SERVICE } from '../data/storage-keys'
|
||||||
import { paramsFromViewState, paramsToViewState } from '../utils/query-params'
|
import { paramsFromViewState, paramsToViewState } from '../utils/query-params'
|
||||||
import {
|
import { DocumentService, SelectionData } from './rest/document.service'
|
||||||
DocumentService,
|
|
||||||
DOCUMENT_SORT_FIELDS,
|
|
||||||
SelectionData,
|
|
||||||
} from './rest/document.service'
|
|
||||||
import { SettingsService } from './settings.service'
|
import { SettingsService } from './settings.service'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -59,6 +60,21 @@ export interface ListViewState {
|
|||||||
* Contains the IDs of all selected documents.
|
* Contains the IDs of all selected documents.
|
||||||
*/
|
*/
|
||||||
selected?: Set<number>
|
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
|
selectionData?: SelectionData
|
||||||
|
|
||||||
currentPageSize: number = this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)
|
|
||||||
|
|
||||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||||
|
|
||||||
private listViewStates: Map<number, ListViewState> = new Map()
|
private listViewStates: Map<number, ListViewState> = new Map()
|
||||||
@ -113,7 +127,7 @@ export class DocumentListViewService {
|
|||||||
delete savedState[k]
|
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)
|
let newState = Object.assign(this.defaultListViewState(), savedState)
|
||||||
this.listViewStates.set(null, newState)
|
this.listViewStates.set(null, newState)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -176,6 +190,9 @@ export class DocumentListViewService {
|
|||||||
if (this._activeSavedViewId) {
|
if (this._activeSavedViewId) {
|
||||||
this.activeListViewState.title = view.name
|
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()
|
this.reduceSelectionToFilter()
|
||||||
|
|
||||||
@ -220,7 +237,7 @@ export class DocumentListViewService {
|
|||||||
this.documentService
|
this.documentService
|
||||||
.listFiltered(
|
.listFiltered(
|
||||||
activeListViewState.currentPage,
|
activeListViewState.currentPage,
|
||||||
this.currentPageSize,
|
activeListViewState.pageSize ?? this.pageSize,
|
||||||
activeListViewState.sortField,
|
activeListViewState.sortField,
|
||||||
activeListViewState.sortReverse,
|
activeListViewState.sortReverse,
|
||||||
activeListViewState.filterRules,
|
activeListViewState.filterRules,
|
||||||
@ -281,9 +298,8 @@ export class DocumentListViewService {
|
|||||||
errorMessage = Object.keys(error.error)
|
errorMessage = Object.keys(error.error)
|
||||||
.map((fieldName) => {
|
.map((fieldName) => {
|
||||||
const fieldError: Array<string> = error.error[fieldName]
|
const fieldError: Array<string> = error.error[fieldName]
|
||||||
return `${DOCUMENT_SORT_FIELDS.find(
|
return `${this.sortFields.find((f) => f.field == fieldName)
|
||||||
(f) => f.field == fieldName
|
?.name}: ${fieldError[0]}`
|
||||||
)?.name}: ${fieldError[0]}`
|
|
||||||
})
|
})
|
||||||
.join(', ')
|
.join(', ')
|
||||||
} else {
|
} else {
|
||||||
@ -312,6 +328,14 @@ export class DocumentListViewService {
|
|||||||
return this.activeListViewState.filterRules
|
return this.activeListViewState.filterRules
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get sortFields(): any[] {
|
||||||
|
return this.documentService.sortFields
|
||||||
|
}
|
||||||
|
|
||||||
|
get sortFieldsFullText(): any[] {
|
||||||
|
return this.documentService.sortFieldsFullText
|
||||||
|
}
|
||||||
|
|
||||||
set sortField(field: string) {
|
set sortField(field: string) {
|
||||||
this.activeListViewState.sortField = field
|
this.activeListViewState.sortField = field
|
||||||
this.reload()
|
this.reload()
|
||||||
@ -362,6 +386,51 @@ export class DocumentListViewService {
|
|||||||
this.saveDocumentListView()
|
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() {
|
private saveDocumentListView() {
|
||||||
if (this._activeSavedViewId == null) {
|
if (this._activeSavedViewId == null) {
|
||||||
let savedState: ListViewState = {
|
let savedState: ListViewState = {
|
||||||
@ -370,6 +439,8 @@ export class DocumentListViewService {
|
|||||||
filterRules: this.activeListViewState.filterRules,
|
filterRules: this.activeListViewState.filterRules,
|
||||||
sortField: this.activeListViewState.sortField,
|
sortField: this.activeListViewState.sortField,
|
||||||
sortReverse: this.activeListViewState.sortReverse,
|
sortReverse: this.activeListViewState.sortReverse,
|
||||||
|
displayMode: this.activeListViewState.displayMode,
|
||||||
|
displayFields: this.activeListViewState.displayFields,
|
||||||
}
|
}
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG,
|
DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG,
|
||||||
@ -385,7 +456,7 @@ export class DocumentListViewService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getLastPage(): number {
|
getLastPage(): number {
|
||||||
return Math.ceil(this.collectionSize / this.currentPageSize)
|
return Math.ceil(this.collectionSize / this.pageSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
hasNext(doc: number) {
|
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() {
|
selectNone() {
|
||||||
this.selected.clear()
|
this.selected.clear()
|
||||||
this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null
|
this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import { Injectable } from '@angular/core'
|
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 { AbstractPaperlessService } from './abstract-paperless-service'
|
||||||
import { Observable } from 'rxjs'
|
|
||||||
import { CustomField } from 'src/app/data/custom-field'
|
import { CustomField } from 'src/app/data/custom-field'
|
||||||
import { CustomFieldInstance } from 'src/app/data/custom-field-instance'
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
|
@ -9,11 +9,17 @@ import { DocumentService } from './document.service'
|
|||||||
import { FILTER_TITLE } from 'src/app/data/filter-rule-type'
|
import { FILTER_TITLE } from 'src/app/data/filter-rule-type'
|
||||||
import { SettingsService } from '../settings.service'
|
import { SettingsService } from '../settings.service'
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
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 httpTestingController: HttpTestingController
|
||||||
let service: DocumentService
|
let service: DocumentService
|
||||||
let subscription: Subscription
|
let subscription: Subscription
|
||||||
let settingsService: SettingsService
|
let settingsService: SettingsService
|
||||||
|
|
||||||
const endpoint = 'documents'
|
const endpoint = 'documents'
|
||||||
const 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(() => {
|
afterEach(() => {
|
||||||
subscription?.unsubscribe()
|
subscription?.unsubscribe()
|
||||||
httpTestingController.verify()
|
httpTestingController.verify()
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import { Injectable } from '@angular/core'
|
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 { DocumentMetadata } from 'src/app/data/document-metadata'
|
||||||
import { AbstractPaperlessService } from './abstract-paperless-service'
|
import { AbstractPaperlessService } from './abstract-paperless-service'
|
||||||
import { HttpClient } from '@angular/common/http'
|
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 { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
import { AuditLogEntry } from 'src/app/data/auditlog-entry'
|
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 {
|
export interface SelectionDataItem {
|
||||||
id: number
|
id: number
|
||||||
document_count: number
|
document_count: number
|
||||||
@ -60,6 +44,16 @@ export interface SelectionData {
|
|||||||
export class DocumentService extends AbstractPaperlessService<Document> {
|
export class DocumentService extends AbstractPaperlessService<Document> {
|
||||||
private _searchQuery: string
|
private _searchQuery: string
|
||||||
|
|
||||||
|
private _sortFields
|
||||||
|
get sortFields() {
|
||||||
|
return this._sortFields
|
||||||
|
}
|
||||||
|
|
||||||
|
private _sortFieldsFullText
|
||||||
|
get sortFieldsFullText() {
|
||||||
|
return this._sortFieldsFullText
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
http: HttpClient,
|
http: HttpClient,
|
||||||
private correspondentService: CorrespondentService,
|
private correspondentService: CorrespondentService,
|
||||||
@ -70,6 +64,46 @@ export class DocumentService extends AbstractPaperlessService<Document> {
|
|||||||
private settingsService: SettingsService
|
private settingsService: SettingsService
|
||||||
) {
|
) {
|
||||||
super(http, 'documents')
|
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) {
|
addObservablesToDocument(doc: Document) {
|
||||||
|
@ -7,17 +7,38 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
|||||||
import { RouterTestingModule } from '@angular/router/testing'
|
import { RouterTestingModule } from '@angular/router/testing'
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { CookieService } from 'ngx-cookie-service'
|
import { CookieService } from 'ngx-cookie-service'
|
||||||
import { Subscription } from 'rxjs'
|
import { Subscription, of } from 'rxjs'
|
||||||
import { environment } from 'src/environments/environment'
|
import { environment } from 'src/environments/environment'
|
||||||
import { AppModule } from '../app.module'
|
import { AppModule } from '../app.module'
|
||||||
import { UiSettings, SETTINGS_KEYS } from '../data/ui-settings'
|
import { UiSettings, SETTINGS_KEYS } from '../data/ui-settings'
|
||||||
import { SettingsService } from './settings.service'
|
import { SettingsService } from './settings.service'
|
||||||
import { SavedView } from '../data/saved-view'
|
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', () => {
|
describe('SettingsService', () => {
|
||||||
let httpTestingController: HttpTestingController
|
let httpTestingController: HttpTestingController
|
||||||
let settingsService: SettingsService
|
let settingsService: SettingsService
|
||||||
let cookieService: CookieService
|
let cookieService: CookieService
|
||||||
|
let customFieldsService: CustomFieldsService
|
||||||
|
let permissionService: PermissionsService
|
||||||
let subscription: Subscription
|
let subscription: Subscription
|
||||||
|
|
||||||
const ui_settings: UiSettings = {
|
const ui_settings: UiSettings = {
|
||||||
@ -76,12 +97,14 @@ describe('SettingsService', () => {
|
|||||||
|
|
||||||
httpTestingController = TestBed.inject(HttpTestingController)
|
httpTestingController = TestBed.inject(HttpTestingController)
|
||||||
cookieService = TestBed.inject(CookieService)
|
cookieService = TestBed.inject(CookieService)
|
||||||
|
customFieldsService = TestBed.inject(CustomFieldsService)
|
||||||
|
permissionService = TestBed.inject(PermissionsService)
|
||||||
settingsService = TestBed.inject(SettingsService)
|
settingsService = TestBed.inject(SettingsService)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
subscription?.unsubscribe()
|
subscription?.unsubscribe()
|
||||||
httpTestingController.verify()
|
// httpTestingController.verify()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('calls ui_settings api endpoint on initialize', () => {
|
it('calls ui_settings api endpoint on initialize', () => {
|
||||||
@ -314,4 +337,51 @@ describe('SettingsService', () => {
|
|||||||
// post for migrate
|
// post for migrate
|
||||||
httpTestingController.expectOne(`${environment.apiBaseUrl}ui_settings/`)
|
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 { environment } from 'src/environments/environment'
|
||||||
import { UiSettings, SETTINGS, SETTINGS_KEYS } from '../data/ui-settings'
|
import { UiSettings, SETTINGS, SETTINGS_KEYS } from '../data/ui-settings'
|
||||||
import { User } from '../data/user'
|
import { User } from '../data/user'
|
||||||
import { PermissionsService } from './permissions.service'
|
import {
|
||||||
|
PermissionAction,
|
||||||
|
PermissionType,
|
||||||
|
PermissionsService,
|
||||||
|
} from './permissions.service'
|
||||||
import { ToastService } from './toast.service'
|
import { ToastService } from './toast.service'
|
||||||
import { SavedView } from '../data/saved-view'
|
import { SavedView } from '../data/saved-view'
|
||||||
|
import { CustomFieldsService } from './rest/custom-fields.service'
|
||||||
|
import { DEFAULT_DISPLAY_FIELDS, DisplayField } from '../data/document'
|
||||||
|
|
||||||
export interface LanguageOption {
|
export interface LanguageOption {
|
||||||
code: string
|
code: string
|
||||||
@ -257,6 +263,12 @@ export class SettingsService {
|
|||||||
public globalDropzoneActive: boolean = false
|
public globalDropzoneActive: boolean = false
|
||||||
public organizingSidebarSavedViews: 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(
|
constructor(
|
||||||
rendererFactory: RendererFactory2,
|
rendererFactory: RendererFactory2,
|
||||||
@Inject(DOCUMENT) private document,
|
@Inject(DOCUMENT) private document,
|
||||||
@ -265,7 +277,8 @@ export class SettingsService {
|
|||||||
@Inject(LOCALE_ID) private localeId: string,
|
@Inject(LOCALE_ID) private localeId: string,
|
||||||
protected http: HttpClient,
|
protected http: HttpClient,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private permissionsService: PermissionsService
|
private permissionsService: PermissionsService,
|
||||||
|
private customFieldsService: CustomFieldsService
|
||||||
) {
|
) {
|
||||||
this._renderer = rendererFactory.createRenderer(null, null)
|
this._renderer = rendererFactory.createRenderer(null, null)
|
||||||
}
|
}
|
||||||
@ -288,10 +301,70 @@ export class SettingsService {
|
|||||||
uisettings.permissions,
|
uisettings.permissions,
|
||||||
this.currentUser
|
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 {
|
get displayName(): string {
|
||||||
return (
|
return (
|
||||||
this.currentUser.first_name ??
|
this.currentUser.first_name ??
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
# Generated by Django 4.2.11 on 2024-04-16 18:35
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("documents", "1046_workflowaction_remove_all_correspondents_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="savedview",
|
||||||
|
name="display_mode",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("table", "Table"),
|
||||||
|
("smallCards", "Small Cards"),
|
||||||
|
("largeCards", "Large Cards"),
|
||||||
|
],
|
||||||
|
max_length=128,
|
||||||
|
null=True,
|
||||||
|
verbose_name="View display mode",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="savedview",
|
||||||
|
name="page_size",
|
||||||
|
field=models.PositiveIntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
validators=[django.core.validators.MinValueValidator(1)],
|
||||||
|
verbose_name="View page size",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="savedview",
|
||||||
|
name="display_fields",
|
||||||
|
field=models.JSONField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Document display fields",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -394,6 +394,25 @@ class Log(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class SavedView(ModelWithOwner):
|
class SavedView(ModelWithOwner):
|
||||||
|
class DisplayMode(models.TextChoices):
|
||||||
|
TABLE = ("table", _("Table"))
|
||||||
|
SMALL_CARDS = ("smallCards", _("Small Cards"))
|
||||||
|
LARGE_CARDS = ("largeCards", _("Large Cards"))
|
||||||
|
|
||||||
|
class DisplayFields(models.TextChoices):
|
||||||
|
TITLE = ("title", _("Title"))
|
||||||
|
CREATED = ("created", _("Created"))
|
||||||
|
ADDED = ("added", _("Added"))
|
||||||
|
TAGS = ("tag"), _("Tags")
|
||||||
|
CORRESPONDENT = ("correspondent", _("Correspondent"))
|
||||||
|
DOCUMENT_TYPE = ("documenttype", _("Document Type"))
|
||||||
|
STORAGE_PATH = ("storagepath", _("Storage Path"))
|
||||||
|
NOTES = ("note", _("Note"))
|
||||||
|
OWNER = ("owner", _("Owner"))
|
||||||
|
SHARED = ("shared", _("Shared"))
|
||||||
|
ASN = ("asn", _("ASN"))
|
||||||
|
CUSTOM_FIELD = ("custom_field_%d", ("Custom Field"))
|
||||||
|
|
||||||
name = models.CharField(_("name"), max_length=128)
|
name = models.CharField(_("name"), max_length=128)
|
||||||
|
|
||||||
show_on_dashboard = models.BooleanField(
|
show_on_dashboard = models.BooleanField(
|
||||||
@ -411,6 +430,27 @@ class SavedView(ModelWithOwner):
|
|||||||
)
|
)
|
||||||
sort_reverse = models.BooleanField(_("sort reverse"), default=False)
|
sort_reverse = models.BooleanField(_("sort reverse"), default=False)
|
||||||
|
|
||||||
|
page_size = models.PositiveIntegerField(
|
||||||
|
_("View page size"),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
validators=[MinValueValidator(1)],
|
||||||
|
)
|
||||||
|
|
||||||
|
display_mode = models.CharField(
|
||||||
|
max_length=128,
|
||||||
|
verbose_name=_("View display mode"),
|
||||||
|
choices=DisplayMode.choices,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
display_fields = models.JSONField(
|
||||||
|
verbose_name=_("Document display fields"),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ("name",)
|
ordering = ("name",)
|
||||||
verbose_name = _("saved view")
|
verbose_name = _("saved view")
|
||||||
|
@ -815,12 +815,33 @@ class SavedViewSerializer(OwnedObjectSerializer):
|
|||||||
"sort_field",
|
"sort_field",
|
||||||
"sort_reverse",
|
"sort_reverse",
|
||||||
"filter_rules",
|
"filter_rules",
|
||||||
|
"page_size",
|
||||||
|
"display_mode",
|
||||||
|
"display_fields",
|
||||||
"owner",
|
"owner",
|
||||||
"permissions",
|
"permissions",
|
||||||
"user_can_change",
|
"user_can_change",
|
||||||
"set_permissions",
|
"set_permissions",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
attrs = super().validate(attrs)
|
||||||
|
if "display_fields" in attrs and attrs["display_fields"] is not None:
|
||||||
|
for field in attrs["display_fields"]:
|
||||||
|
if (
|
||||||
|
SavedView.DisplayFields.CUSTOM_FIELD[:-2] in field
|
||||||
|
): # i.e. check for 'custom_field_' prefix
|
||||||
|
field_id = int(re.search(r"\d+", field)[0])
|
||||||
|
if not CustomField.objects.filter(id=field_id).exists():
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
f"Invalid field: {field}",
|
||||||
|
)
|
||||||
|
elif field not in SavedView.DisplayFields.values:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
f"Invalid field: {field}",
|
||||||
|
)
|
||||||
|
return attrs
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
if "filter_rules" in validated_data:
|
if "filter_rules" in validated_data:
|
||||||
rules_data = validated_data.pop("filter_rules")
|
rules_data = validated_data.pop("filter_rules")
|
||||||
|
@ -1614,7 +1614,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
|||||||
status.HTTP_404_NOT_FOUND,
|
status.HTTP_404_NOT_FOUND,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_create_update_patch(self):
|
def test_saved_view_create_update_patch(self):
|
||||||
User.objects.create_user("user1")
|
User.objects.create_user("user1")
|
||||||
|
|
||||||
view = {
|
view = {
|
||||||
@ -1661,6 +1661,155 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
|||||||
v1 = SavedView.objects.get(id=v1.id)
|
v1 = SavedView.objects.get(id=v1.id)
|
||||||
self.assertEqual(v1.filter_rules.count(), 0)
|
self.assertEqual(v1.filter_rules.count(), 0)
|
||||||
|
|
||||||
|
def test_saved_view_display_options(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Saved view
|
||||||
|
WHEN:
|
||||||
|
- Updating display options
|
||||||
|
THEN:
|
||||||
|
- Display options are updated
|
||||||
|
- Display fields are validated
|
||||||
|
"""
|
||||||
|
User.objects.create_user("user1")
|
||||||
|
|
||||||
|
view = {
|
||||||
|
"name": "test",
|
||||||
|
"show_on_dashboard": True,
|
||||||
|
"show_in_sidebar": True,
|
||||||
|
"sort_field": "created2",
|
||||||
|
"filter_rules": [{"rule_type": 4, "value": "test"}],
|
||||||
|
"page_size": 20,
|
||||||
|
"display_mode": SavedView.DisplayMode.SMALL_CARDS,
|
||||||
|
"display_fields": [
|
||||||
|
SavedView.DisplayFields.TITLE,
|
||||||
|
SavedView.DisplayFields.CREATED,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post("/api/saved_views/", view, format="json")
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
v1 = SavedView.objects.get(name="test")
|
||||||
|
self.assertEqual(v1.page_size, 20)
|
||||||
|
self.assertEqual(
|
||||||
|
v1.display_mode,
|
||||||
|
SavedView.DisplayMode.SMALL_CARDS,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
v1.display_fields,
|
||||||
|
[
|
||||||
|
SavedView.DisplayFields.TITLE,
|
||||||
|
SavedView.DisplayFields.CREATED,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.patch(
|
||||||
|
f"/api/saved_views/{v1.id}/",
|
||||||
|
{
|
||||||
|
"display_fields": [
|
||||||
|
SavedView.DisplayFields.TAGS,
|
||||||
|
SavedView.DisplayFields.TITLE,
|
||||||
|
SavedView.DisplayFields.CREATED,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
v1.refresh_from_db()
|
||||||
|
self.assertEqual(
|
||||||
|
v1.display_fields,
|
||||||
|
[
|
||||||
|
SavedView.DisplayFields.TAGS,
|
||||||
|
SavedView.DisplayFields.TITLE,
|
||||||
|
SavedView.DisplayFields.CREATED,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Invalid display field
|
||||||
|
response = self.client.patch(
|
||||||
|
f"/api/saved_views/{v1.id}/",
|
||||||
|
{
|
||||||
|
"display_fields": [
|
||||||
|
"foobar",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def test_saved_view_display_customfields(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Saved view
|
||||||
|
WHEN:
|
||||||
|
- Updating display options with custom fields
|
||||||
|
THEN:
|
||||||
|
- Display filds for custom fields are updated
|
||||||
|
- Display fields for custom fields are validated
|
||||||
|
"""
|
||||||
|
view = {
|
||||||
|
"name": "test",
|
||||||
|
"show_on_dashboard": True,
|
||||||
|
"show_in_sidebar": True,
|
||||||
|
"sort_field": "created2",
|
||||||
|
"filter_rules": [{"rule_type": 4, "value": "test"}],
|
||||||
|
"page_size": 20,
|
||||||
|
"display_mode": SavedView.DisplayMode.SMALL_CARDS,
|
||||||
|
"display_fields": [
|
||||||
|
SavedView.DisplayFields.TITLE,
|
||||||
|
SavedView.DisplayFields.CREATED,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post("/api/saved_views/", view, format="json")
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
v1 = SavedView.objects.get(name="test")
|
||||||
|
|
||||||
|
custom_field = CustomField.objects.create(
|
||||||
|
name="stringfield",
|
||||||
|
data_type=CustomField.FieldDataType.STRING,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.patch(
|
||||||
|
f"/api/saved_views/{v1.id}/",
|
||||||
|
{
|
||||||
|
"display_fields": [
|
||||||
|
SavedView.DisplayFields.TITLE,
|
||||||
|
SavedView.DisplayFields.CREATED,
|
||||||
|
SavedView.DisplayFields.CUSTOM_FIELD % custom_field.id,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
v1.refresh_from_db()
|
||||||
|
self.assertEqual(
|
||||||
|
v1.display_fields,
|
||||||
|
[
|
||||||
|
str(SavedView.DisplayFields.TITLE),
|
||||||
|
str(SavedView.DisplayFields.CREATED),
|
||||||
|
SavedView.DisplayFields.CUSTOM_FIELD % custom_field.id,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Custom field not found
|
||||||
|
response = self.client.patch(
|
||||||
|
f"/api/saved_views/{v1.id}/",
|
||||||
|
{
|
||||||
|
"display_fields": [
|
||||||
|
SavedView.DisplayFields.TITLE,
|
||||||
|
SavedView.DisplayFields.CREATED,
|
||||||
|
SavedView.DisplayFields.CUSTOM_FIELD % 99,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def test_get_logs(self):
|
def test_get_logs(self):
|
||||||
log_data = "test\ntest2\n"
|
log_data = "test\ntest2\n"
|
||||||
with open(os.path.join(settings.LOGGING_DIR, "mail.log"), "w") as f:
|
with open(os.path.join(settings.LOGGING_DIR, "mail.log"), "w") as f:
|
||||||
|
@ -2,7 +2,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: paperless-ngx\n"
|
"Project-Id-Version: paperless-ngx\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-04-19 01:15-0700\n"
|
"POT-Creation-Date: 2024-04-24 22:54-0700\n"
|
||||||
"PO-Revision-Date: 2022-02-17 04:17\n"
|
"PO-Revision-Date: 2022-02-17 04:17\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: English\n"
|
"Language-Team: English\n"
|
||||||
@ -21,31 +21,31 @@ msgstr ""
|
|||||||
msgid "Documents"
|
msgid "Documents"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:36 documents/models.py:739
|
#: documents/models.py:36 documents/models.py:779
|
||||||
msgid "owner"
|
msgid "owner"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:53 documents/models.py:902
|
#: documents/models.py:53 documents/models.py:942
|
||||||
msgid "None"
|
msgid "None"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:54 documents/models.py:903
|
#: documents/models.py:54 documents/models.py:943
|
||||||
msgid "Any word"
|
msgid "Any word"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:55 documents/models.py:904
|
#: documents/models.py:55 documents/models.py:944
|
||||||
msgid "All words"
|
msgid "All words"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:56 documents/models.py:905
|
#: documents/models.py:56 documents/models.py:945
|
||||||
msgid "Exact match"
|
msgid "Exact match"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:57 documents/models.py:906
|
#: documents/models.py:57 documents/models.py:946
|
||||||
msgid "Regular expression"
|
msgid "Regular expression"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:58 documents/models.py:907
|
#: documents/models.py:58 documents/models.py:947
|
||||||
msgid "Fuzzy word"
|
msgid "Fuzzy word"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -53,20 +53,20 @@ msgstr ""
|
|||||||
msgid "Automatic"
|
msgid "Automatic"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:62 documents/models.py:397 documents/models.py:1223
|
#: documents/models.py:62 documents/models.py:416 documents/models.py:1263
|
||||||
#: paperless_mail/models.py:18 paperless_mail/models.py:93
|
#: paperless_mail/models.py:18 paperless_mail/models.py:93
|
||||||
msgid "name"
|
msgid "name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:64 documents/models.py:963
|
#: documents/models.py:64 documents/models.py:1003
|
||||||
msgid "match"
|
msgid "match"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:67 documents/models.py:966
|
#: documents/models.py:67 documents/models.py:1006
|
||||||
msgid "matching algorithm"
|
msgid "matching algorithm"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:72 documents/models.py:971
|
#: documents/models.py:72 documents/models.py:1011
|
||||||
msgid "is insensitive"
|
msgid "is insensitive"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -132,7 +132,7 @@ msgstr ""
|
|||||||
msgid "title"
|
msgid "title"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:171 documents/models.py:653
|
#: documents/models.py:171 documents/models.py:693
|
||||||
msgid "content"
|
msgid "content"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -162,8 +162,8 @@ msgstr ""
|
|||||||
msgid "The checksum of the archived document."
|
msgid "The checksum of the archived document."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:205 documents/models.py:385 documents/models.py:659
|
#: documents/models.py:205 documents/models.py:385 documents/models.py:699
|
||||||
#: documents/models.py:697 documents/models.py:767 documents/models.py:804
|
#: documents/models.py:737 documents/models.py:807 documents/models.py:844
|
||||||
msgid "created"
|
msgid "created"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -211,7 +211,7 @@ msgstr ""
|
|||||||
msgid "The position of this document in your physical document archive."
|
msgid "The position of this document in your physical document archive."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:279 documents/models.py:670 documents/models.py:724
|
#: documents/models.py:279 documents/models.py:710 documents/models.py:764
|
||||||
msgid "document"
|
msgid "document"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -259,584 +259,652 @@ msgstr ""
|
|||||||
msgid "logs"
|
msgid "logs"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:398
|
||||||
|
msgid "Table"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:399
|
||||||
|
msgid "Small Cards"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:400
|
#: documents/models.py:400
|
||||||
msgid "show on dashboard"
|
msgid "Large Cards"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:403
|
#: documents/models.py:403
|
||||||
msgid "show in sidebar"
|
msgid "Title"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:404
|
||||||
|
msgid "Created"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:405
|
||||||
|
msgid "Added"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:406
|
||||||
|
msgid "Tags"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:407
|
#: documents/models.py:407
|
||||||
msgid "sort field"
|
msgid "Correspondent"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:408
|
||||||
|
msgid "Document Type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:409
|
||||||
|
msgid "Storage Path"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:410
|
||||||
|
msgid "Note"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:411
|
||||||
|
msgid "Owner"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:412
|
#: documents/models.py:412
|
||||||
msgid "sort reverse"
|
msgid "Shared"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:416 documents/models.py:469
|
#: documents/models.py:413
|
||||||
msgid "saved view"
|
msgid "ASN"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:417
|
#: documents/models.py:419
|
||||||
msgid "saved views"
|
msgid "show on dashboard"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:425
|
#: documents/models.py:422
|
||||||
msgid "title contains"
|
msgid "show in sidebar"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:426
|
#: documents/models.py:426
|
||||||
msgid "content contains"
|
msgid "sort field"
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:427
|
|
||||||
msgid "ASN is"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:428
|
|
||||||
msgid "correspondent is"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:429
|
|
||||||
msgid "document type is"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:430
|
|
||||||
msgid "is in inbox"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:431
|
#: documents/models.py:431
|
||||||
msgid "has tag"
|
msgid "sort reverse"
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:432
|
|
||||||
msgid "has any tag"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:433
|
|
||||||
msgid "created before"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:434
|
#: documents/models.py:434
|
||||||
msgid "created after"
|
msgid "View page size"
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:435
|
|
||||||
msgid "created year is"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:436
|
|
||||||
msgid "created month is"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:437
|
|
||||||
msgid "created day is"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:438
|
|
||||||
msgid "added before"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:439
|
|
||||||
msgid "added after"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:440
|
|
||||||
msgid "modified before"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:441
|
|
||||||
msgid "modified after"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:442
|
#: documents/models.py:442
|
||||||
msgid "does not have tag"
|
msgid "View display mode"
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:443
|
|
||||||
msgid "does not have ASN"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:444
|
|
||||||
msgid "title or content contains"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:445
|
|
||||||
msgid "fulltext query"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:446
|
|
||||||
msgid "more like this"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:447
|
|
||||||
msgid "has tags in"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:448
|
|
||||||
msgid "ASN greater than"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:449
|
#: documents/models.py:449
|
||||||
msgid "ASN less than"
|
msgid "Document display fields"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:450
|
#: documents/models.py:456 documents/models.py:509
|
||||||
msgid "storage path is"
|
msgid "saved view"
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:451
|
|
||||||
msgid "has correspondent in"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:452
|
|
||||||
msgid "does not have correspondent in"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:453
|
|
||||||
msgid "has document type in"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:454
|
|
||||||
msgid "does not have document type in"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:455
|
|
||||||
msgid "has storage path in"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:456
|
|
||||||
msgid "does not have storage path in"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:457
|
#: documents/models.py:457
|
||||||
msgid "owner is"
|
msgid "saved views"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:458
|
#: documents/models.py:465
|
||||||
msgid "has owner in"
|
msgid "title contains"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:459
|
#: documents/models.py:466
|
||||||
msgid "does not have owner"
|
msgid "content contains"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:460
|
#: documents/models.py:467
|
||||||
msgid "does not have owner in"
|
msgid "ASN is"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:461
|
#: documents/models.py:468
|
||||||
msgid "has custom field value"
|
msgid "correspondent is"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:462
|
#: documents/models.py:469
|
||||||
msgid "is shared by me"
|
msgid "document type is"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:470
|
||||||
|
msgid "is in inbox"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:471
|
||||||
|
msgid "has tag"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:472
|
#: documents/models.py:472
|
||||||
msgid "rule type"
|
msgid "has any tag"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:473
|
||||||
|
msgid "created before"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:474
|
#: documents/models.py:474
|
||||||
msgid "value"
|
msgid "created after"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:475
|
||||||
|
msgid "created year is"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:476
|
||||||
|
msgid "created month is"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:477
|
#: documents/models.py:477
|
||||||
msgid "filter rule"
|
msgid "created day is"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:478
|
#: documents/models.py:478
|
||||||
|
msgid "added before"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:479
|
||||||
|
msgid "added after"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:480
|
||||||
|
msgid "modified before"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:481
|
||||||
|
msgid "modified after"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:482
|
||||||
|
msgid "does not have tag"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:483
|
||||||
|
msgid "does not have ASN"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:484
|
||||||
|
msgid "title or content contains"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:485
|
||||||
|
msgid "fulltext query"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:486
|
||||||
|
msgid "more like this"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:487
|
||||||
|
msgid "has tags in"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:488
|
||||||
|
msgid "ASN greater than"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:489
|
||||||
|
msgid "ASN less than"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:490
|
||||||
|
msgid "storage path is"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:491
|
||||||
|
msgid "has correspondent in"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:492
|
||||||
|
msgid "does not have correspondent in"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:493
|
||||||
|
msgid "has document type in"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:494
|
||||||
|
msgid "does not have document type in"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:495
|
||||||
|
msgid "has storage path in"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:496
|
||||||
|
msgid "does not have storage path in"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:497
|
||||||
|
msgid "owner is"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:498
|
||||||
|
msgid "has owner in"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:499
|
||||||
|
msgid "does not have owner"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:500
|
||||||
|
msgid "does not have owner in"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:501
|
||||||
|
msgid "has custom field value"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:502
|
||||||
|
msgid "is shared by me"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:512
|
||||||
|
msgid "rule type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:514
|
||||||
|
msgid "value"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:517
|
||||||
|
msgid "filter rule"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:518
|
||||||
msgid "filter rules"
|
msgid "filter rules"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:589
|
#: documents/models.py:629
|
||||||
msgid "Task ID"
|
msgid "Task ID"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:590
|
#: documents/models.py:630
|
||||||
msgid "Celery ID for the Task that was run"
|
msgid "Celery ID for the Task that was run"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:595
|
#: documents/models.py:635
|
||||||
msgid "Acknowledged"
|
msgid "Acknowledged"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:596
|
#: documents/models.py:636
|
||||||
msgid "If the task is acknowledged via the frontend or API"
|
msgid "If the task is acknowledged via the frontend or API"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:602
|
#: documents/models.py:642
|
||||||
msgid "Task Filename"
|
msgid "Task Filename"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:603
|
#: documents/models.py:643
|
||||||
msgid "Name of the file which the Task was run for"
|
msgid "Name of the file which the Task was run for"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:609
|
#: documents/models.py:649
|
||||||
msgid "Task Name"
|
msgid "Task Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:610
|
#: documents/models.py:650
|
||||||
msgid "Name of the Task which was run"
|
msgid "Name of the Task which was run"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:617
|
#: documents/models.py:657
|
||||||
msgid "Task State"
|
msgid "Task State"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:618
|
#: documents/models.py:658
|
||||||
msgid "Current state of the task being run"
|
msgid "Current state of the task being run"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:623
|
#: documents/models.py:663
|
||||||
msgid "Created DateTime"
|
msgid "Created DateTime"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:624
|
#: documents/models.py:664
|
||||||
msgid "Datetime field when the task result was created in UTC"
|
msgid "Datetime field when the task result was created in UTC"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:629
|
#: documents/models.py:669
|
||||||
msgid "Started DateTime"
|
msgid "Started DateTime"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:630
|
#: documents/models.py:670
|
||||||
msgid "Datetime field when the task was started in UTC"
|
msgid "Datetime field when the task was started in UTC"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:635
|
#: documents/models.py:675
|
||||||
msgid "Completed DateTime"
|
msgid "Completed DateTime"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:636
|
#: documents/models.py:676
|
||||||
msgid "Datetime field when the task was completed in UTC"
|
msgid "Datetime field when the task was completed in UTC"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:641
|
#: documents/models.py:681
|
||||||
msgid "Result Data"
|
msgid "Result Data"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:643
|
#: documents/models.py:683
|
||||||
msgid "The data returned by the task"
|
msgid "The data returned by the task"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:655
|
#: documents/models.py:695
|
||||||
msgid "Note for the document"
|
msgid "Note for the document"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:679
|
#: documents/models.py:719
|
||||||
msgid "user"
|
msgid "user"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:684
|
#: documents/models.py:724
|
||||||
msgid "note"
|
msgid "note"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:685
|
#: documents/models.py:725
|
||||||
msgid "notes"
|
msgid "notes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:693
|
#: documents/models.py:733
|
||||||
msgid "Archive"
|
msgid "Archive"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:694
|
#: documents/models.py:734
|
||||||
msgid "Original"
|
msgid "Original"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:705
|
#: documents/models.py:745
|
||||||
msgid "expiration"
|
msgid "expiration"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:712
|
#: documents/models.py:752
|
||||||
msgid "slug"
|
msgid "slug"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:744
|
#: documents/models.py:784
|
||||||
msgid "share link"
|
msgid "share link"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:745
|
#: documents/models.py:785
|
||||||
msgid "share links"
|
msgid "share links"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:757
|
#: documents/models.py:797
|
||||||
msgid "String"
|
msgid "String"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:758
|
#: documents/models.py:798
|
||||||
msgid "URL"
|
msgid "URL"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:759
|
#: documents/models.py:799
|
||||||
msgid "Date"
|
msgid "Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:760
|
#: documents/models.py:800
|
||||||
msgid "Boolean"
|
msgid "Boolean"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:761
|
#: documents/models.py:801
|
||||||
msgid "Integer"
|
msgid "Integer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:762
|
#: documents/models.py:802
|
||||||
msgid "Float"
|
msgid "Float"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:763
|
#: documents/models.py:803
|
||||||
msgid "Monetary"
|
msgid "Monetary"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:764
|
#: documents/models.py:804
|
||||||
msgid "Document Link"
|
msgid "Document Link"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:776
|
#: documents/models.py:816
|
||||||
msgid "data type"
|
msgid "data type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:784
|
#: documents/models.py:824
|
||||||
msgid "custom field"
|
msgid "custom field"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:785
|
#: documents/models.py:825
|
||||||
msgid "custom fields"
|
msgid "custom fields"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:847
|
#: documents/models.py:887
|
||||||
msgid "custom field instance"
|
msgid "custom field instance"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:848
|
#: documents/models.py:888
|
||||||
msgid "custom field instances"
|
msgid "custom field instances"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:910
|
#: documents/models.py:950
|
||||||
msgid "Consumption Started"
|
msgid "Consumption Started"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:911
|
#: documents/models.py:951
|
||||||
msgid "Document Added"
|
msgid "Document Added"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:912
|
#: documents/models.py:952
|
||||||
msgid "Document Updated"
|
msgid "Document Updated"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:915
|
#: documents/models.py:955
|
||||||
msgid "Consume Folder"
|
msgid "Consume Folder"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:916
|
#: documents/models.py:956
|
||||||
msgid "Api Upload"
|
msgid "Api Upload"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:917
|
#: documents/models.py:957
|
||||||
msgid "Mail Fetch"
|
msgid "Mail Fetch"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:920
|
#: documents/models.py:960
|
||||||
msgid "Workflow Trigger Type"
|
msgid "Workflow Trigger Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:932
|
#: documents/models.py:972
|
||||||
msgid "filter path"
|
msgid "filter path"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:937
|
#: documents/models.py:977
|
||||||
msgid ""
|
msgid ""
|
||||||
"Only consume documents with a path that matches this if specified. Wildcards "
|
"Only consume documents with a path that matches this if specified. Wildcards "
|
||||||
"specified as * are allowed. Case insensitive."
|
"specified as * are allowed. Case insensitive."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:944
|
#: documents/models.py:984
|
||||||
msgid "filter filename"
|
msgid "filter filename"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:949 paperless_mail/models.py:148
|
#: documents/models.py:989 paperless_mail/models.py:148
|
||||||
msgid ""
|
msgid ""
|
||||||
"Only consume documents which entirely match this filename if specified. "
|
"Only consume documents which entirely match this filename if specified. "
|
||||||
"Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
|
"Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:960
|
#: documents/models.py:1000
|
||||||
msgid "filter documents from this mail rule"
|
msgid "filter documents from this mail rule"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:976
|
#: documents/models.py:1016
|
||||||
msgid "has these tag(s)"
|
msgid "has these tag(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:984
|
#: documents/models.py:1024
|
||||||
msgid "has this document type"
|
msgid "has this document type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:992
|
#: documents/models.py:1032
|
||||||
msgid "has this correspondent"
|
msgid "has this correspondent"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:996
|
#: documents/models.py:1036
|
||||||
msgid "workflow trigger"
|
msgid "workflow trigger"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:997
|
#: documents/models.py:1037
|
||||||
msgid "workflow triggers"
|
msgid "workflow triggers"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1007
|
#: documents/models.py:1047
|
||||||
msgid "Assignment"
|
msgid "Assignment"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1011
|
#: documents/models.py:1051
|
||||||
msgid "Removal"
|
msgid "Removal"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1015
|
#: documents/models.py:1055
|
||||||
msgid "Workflow Action Type"
|
msgid "Workflow Action Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1021
|
#: documents/models.py:1061
|
||||||
msgid "assign title"
|
msgid "assign title"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1026
|
#: documents/models.py:1066
|
||||||
msgid ""
|
msgid ""
|
||||||
"Assign a document title, can include some placeholders, see documentation."
|
"Assign a document title, can include some placeholders, see documentation."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1035 paperless_mail/models.py:216
|
#: documents/models.py:1075 paperless_mail/models.py:216
|
||||||
msgid "assign this tag"
|
msgid "assign this tag"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1044 paperless_mail/models.py:224
|
#: documents/models.py:1084 paperless_mail/models.py:224
|
||||||
msgid "assign this document type"
|
msgid "assign this document type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1053 paperless_mail/models.py:238
|
#: documents/models.py:1093 paperless_mail/models.py:238
|
||||||
msgid "assign this correspondent"
|
msgid "assign this correspondent"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1062
|
#: documents/models.py:1102
|
||||||
msgid "assign this storage path"
|
msgid "assign this storage path"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1071
|
#: documents/models.py:1111
|
||||||
msgid "assign this owner"
|
msgid "assign this owner"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1078
|
#: documents/models.py:1118
|
||||||
msgid "grant view permissions to these users"
|
msgid "grant view permissions to these users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1085
|
#: documents/models.py:1125
|
||||||
msgid "grant view permissions to these groups"
|
msgid "grant view permissions to these groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1092
|
#: documents/models.py:1132
|
||||||
msgid "grant change permissions to these users"
|
msgid "grant change permissions to these users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1099
|
#: documents/models.py:1139
|
||||||
msgid "grant change permissions to these groups"
|
msgid "grant change permissions to these groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1106
|
#: documents/models.py:1146
|
||||||
msgid "assign these custom fields"
|
msgid "assign these custom fields"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1113
|
#: documents/models.py:1153
|
||||||
msgid "remove these tag(s)"
|
msgid "remove these tag(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1118
|
#: documents/models.py:1158
|
||||||
msgid "remove all tags"
|
msgid "remove all tags"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1125
|
#: documents/models.py:1165
|
||||||
msgid "remove these document type(s)"
|
msgid "remove these document type(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1130
|
#: documents/models.py:1170
|
||||||
msgid "remove all document types"
|
msgid "remove all document types"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1137
|
#: documents/models.py:1177
|
||||||
msgid "remove these correspondent(s)"
|
msgid "remove these correspondent(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1142
|
#: documents/models.py:1182
|
||||||
msgid "remove all correspondents"
|
msgid "remove all correspondents"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1149
|
#: documents/models.py:1189
|
||||||
msgid "remove these storage path(s)"
|
msgid "remove these storage path(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1154
|
#: documents/models.py:1194
|
||||||
msgid "remove all storage paths"
|
msgid "remove all storage paths"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1161
|
#: documents/models.py:1201
|
||||||
msgid "remove these owner(s)"
|
msgid "remove these owner(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1166
|
#: documents/models.py:1206
|
||||||
msgid "remove all owners"
|
msgid "remove all owners"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1173
|
#: documents/models.py:1213
|
||||||
msgid "remove view permissions for these users"
|
msgid "remove view permissions for these users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1180
|
#: documents/models.py:1220
|
||||||
msgid "remove view permissions for these groups"
|
msgid "remove view permissions for these groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1187
|
#: documents/models.py:1227
|
||||||
msgid "remove change permissions for these users"
|
msgid "remove change permissions for these users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1194
|
#: documents/models.py:1234
|
||||||
msgid "remove change permissions for these groups"
|
msgid "remove change permissions for these groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1199
|
#: documents/models.py:1239
|
||||||
msgid "remove all permissions"
|
msgid "remove all permissions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1206
|
#: documents/models.py:1246
|
||||||
msgid "remove these custom fields"
|
msgid "remove these custom fields"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1211
|
#: documents/models.py:1251
|
||||||
msgid "remove all custom fields"
|
msgid "remove all custom fields"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1215
|
#: documents/models.py:1255
|
||||||
msgid "workflow action"
|
msgid "workflow action"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1216
|
#: documents/models.py:1256
|
||||||
msgid "workflow actions"
|
msgid "workflow actions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1225 paperless_mail/models.py:95
|
#: documents/models.py:1265 paperless_mail/models.py:95
|
||||||
msgid "order"
|
msgid "order"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1231
|
#: documents/models.py:1271
|
||||||
msgid "triggers"
|
msgid "triggers"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1238
|
#: documents/models.py:1278
|
||||||
msgid "actions"
|
msgid "actions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1241
|
#: documents/models.py:1281
|
||||||
msgid "enabled"
|
msgid "enabled"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -849,12 +917,12 @@ msgstr ""
|
|||||||
msgid "Invalid color."
|
msgid "Invalid color."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/serialisers.py:1148
|
#: documents/serialisers.py:1169
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "File type %(type)s not supported"
|
msgid "File type %(type)s not supported"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/serialisers.py:1257
|
#: documents/serialisers.py:1278
|
||||||
msgid "Invalid variable detected."
|
msgid "Invalid variable detected."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user