Customizable dashboard views

This commit is contained in:
shamoon 2024-04-16 20:19:58 -07:00
parent 8e39315586
commit bf88d232fb
13 changed files with 452 additions and 72 deletions

View File

@ -124,7 +124,7 @@
"content": {
"size": -1,
"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,\"dashboard_view_limit\":10,\"dashboard_view_mode\":\"table\",\"dashboard_view_table_columns\":[\"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,\"dashboard_view_limit\":10,\"dashboard_view_mode\":\"table\",\"dashboard_view_table_columns\":[\"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,\"dashboard_view_limit\":10,\"dashboard_view_mode\":\"table\",\"dashboard_view_table_columns\":[\"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,\"dashboard_view_limit\":10,\"dashboard_view_mode\":\"table\",\"dashboard_view_table_columns\":[\"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,\"dashboard_view_limit\":10,\"dashboard_view_mode\":\"table\",\"dashboard_view_table_columns\":[\"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,\"dashboard_view_limit\":10,\"dashboard_view_mode\":\"table\",\"dashboard_view_table_columns\":[\"created\",\"title\",\"tag\",\"documenttype\"]}]}"
},
"headersSize": -1,
"bodySize": -1,

View File

@ -124,7 +124,7 @@
"content": {
"size": -1,
"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,\"dashboard_view_limit\":10,\"dashboard_view_mode\":\"table\",\"dashboard_view_table_columns\":[\"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,\"dashboard_view_limit\":10,\"dashboard_view_mode\":\"table\",\"dashboard_view_table_columns\":[\"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,\"dashboard_view_limit\":10,\"dashboard_view_mode\":\"table\",\"dashboard_view_table_columns\":[\"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,\"dashboard_view_limit\":10,\"dashboard_view_mode\":\"table\",\"dashboard_view_table_columns\":[\"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,\"dashboard_view_limit\":10,\"dashboard_view_mode\":\"table\",\"dashboard_view_table_columns\":[\"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,\"dashboard_view_limit\":10,\"dashboard_view_mode\":\"table\",\"dashboard_view_table_columns\":[\"created\",\"title\",\"tag\",\"documenttype\"]}]}"
},
"headersSize": -1,
"bodySize": -1,

View File

@ -124,7 +124,7 @@
"content": {
"size": -1,
"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,\"dashboard_view_limit\":10,\"dashboard_view_mode\":\"table\",\"dashboard_view_table_columns\":[\"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,\"dashboard_view_limit\":10,\"dashboard_view_mode\":\"table\",\"dashboard_view_table_columns\":[\"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,\"dashboard_view_limit\":10,\"dashboard_view_mode\":\"table\",\"dashboard_view_table_columns\":[\"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,\"dashboard_view_limit\":10,\"dashboard_view_mode\":\"table\",\"dashboard_view_table_columns\":[\"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,\"dashboard_view_limit\":10,\"dashboard_view_mode\":\"table\",\"dashboard_view_table_columns\":[\"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,\"dashboard_view_limit\":10,\"dashboard_view_mode\":\"table\",\"dashboard_view_table_columns\":[\"created\",\"title\",\"tag\",\"documenttype\"]}]}"
},
"headersSize": -1,
"bodySize": -1,

View File

@ -124,7 +124,7 @@
"content": {
"size": -1,
"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,\"dashboard_view_limit\":10,\"dashboard_view_mode\":\"table\",\"dashboard_view_table_columns\":[\"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,\"dashboard_view_limit\":10,\"dashboard_view_mode\":\"table\",\"dashboard_view_table_columns\":[\"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,\"dashboard_view_limit\":10,\"dashboard_view_mode\":\"table\",\"dashboard_view_table_columns\":[\"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,\"dashboard_view_limit\":10,\"dashboard_view_mode\":\"table\",\"dashboard_view_table_columns\":[\"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,\"dashboard_view_limit\":10,\"dashboard_view_mode\":\"table\",\"dashboard_view_table_columns\":[\"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,\"dashboard_view_limit\":10,\"dashboard_view_mode\":\"table\",\"dashboard_view_table_columns\":[\"created\",\"title\",\"tag\",\"documenttype\"]}]}"
},
"headersSize": -1,
"bodySize": -1,

View File

@ -9,40 +9,62 @@
<a class="btn-link text-decoration-none" header-buttons [routerLink]="[]" (click)="showAll()" i18n>Show all</a>
}
@if (documents.length) {
<table content class="table table-hover mb-0 align-middle">
@if (documents.length && savedView.dashboard_view_mode === DashboardViewMode.TABLE) {
<table content class="table table-hover mb-0 mt-n2 align-middle">
<thead>
<tr>
<th scope="col" i18n>Created</th>
<th scope="col" i18n>Title</th>
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Tag)) {
<th scope="col" class="d-none d-md-table-cell" i18n>Tags</th>
@for (column of savedView.dashboard_view_table_columns; track column; let i = $index) {
@if (columnIsVisible(column)) {
<th
scope="col"
[ngClass]="{
'd-none d-md-table-cell': i > 1,
'w-25': column === DashboardViewTableColumn.CREATED || column === DashboardViewTableColumn.ADDED
}">
{{ getColumnTitle(column) }}
</th>
}
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
<th scope="col" class="d-none d-md-table-cell" i18n>Correspondent</th>
} @else {
<th scope="col" class="d-none d-md-table-cell"></th>
}
</tr>
</thead>
<tbody>
@for (doc of documents; track doc) {
<tr (mouseleave)="maybeClosePopover()">
<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>
<td class="py-2 py-md-3">
@for (column of savedView.dashboard_view_table_columns; track column; let i = $index) {
@if (columnIsVisible(column)) {
<td class="py-2 py-md-3 position-relative" [ngClass]="{ 'd-none d-md-table-cell': i > 1 }">
@switch (column) {
@case (DashboardViewTableColumn.ADDED) {
<a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.added | customDate}}</a>
}
@case (DashboardViewTableColumn.CREATED) {
<a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.created_date | customDate}}</a>
}
@case (DashboardViewTableColumn.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>
</td>
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Tag)) {
<td class="py-2 py-md-3 d-none d-md-table-cell">
}
@case (DashboardViewTableColumn.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 (DashboardViewTableColumn.TAGS) {
@for (t of doc.tags$ | async; track t) {
<pngx-tag [tag]="t" class="ms-1" (click)="clickTag(t, $event)"></pngx-tag>
<pngx-tag [tag]="t" class="ms-1" (click)="clickTag(t.id, $event)"></pngx-tag>
}
</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>
@case (DashboardViewTableColumn.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 (DashboardViewTableColumn.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 (i === savedView.dashboard_view_table_columns.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"
@ -56,11 +78,20 @@
<i-bs width="0.8em" height="0.8em" name="download"></i-bs>
</a>
</div>
}
</td>
}
}
</tr>
}
</tbody>
</table>
} @else if (documents.length && savedView.dashboard_view_mode === DashboardViewMode.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" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickStoragePath)="clickStoragePath($event)" (clickDocumentType)="clickDocumentType($event)"></pngx-document-card-small>
}
</div>
} @else {
<p i18n class="text-center text-muted mb-0 fst-italic">No documents</p>
}

View File

@ -3,10 +3,9 @@ table {
table-layout: fixed;
}
th:first-child {
width: 25%;
@media (min-width: 768px) {
width: 15%;
th.w-25 {
width: 15% !important;
}
}
@ -30,3 +29,45 @@ td.py-3 {
padding-top: 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
}

View File

@ -11,8 +11,17 @@ import { RouterTestingModule } from '@angular/router/testing'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { of, Subject } from 'rxjs'
import { routes } from 'src/app/app-routing.module'
import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type'
import { SavedView } from 'src/app/data/saved-view'
import {
FILTER_CORRESPONDENT,
FILTER_DOCUMENT_TYPE,
FILTER_HAS_TAGS_ALL,
FILTER_STORAGE_PATH,
} from 'src/app/data/filter-rule-type'
import {
DashboardViewMode,
DashboardViewTableColumn,
SavedView,
} from 'src/app/data/saved-view'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
@ -45,6 +54,14 @@ const savedView: SavedView = {
value: '1,2',
},
],
dashboard_view_limit: 20,
dashboard_view_mode: DashboardViewMode.TABLE,
dashboard_view_table_columns: [
DashboardViewTableColumn.CREATED,
DashboardViewTableColumn.TITLE,
DashboardViewTableColumn.TAGS,
DashboardViewTableColumn.CORRESPONDENT,
],
}
const documentResults = [
@ -170,7 +187,7 @@ describe('SavedViewWidgetComponent', () => {
component.ngOnInit()
expect(listAllSpy).toHaveBeenCalledWith(
1,
10,
20,
savedView.sort_field,
savedView.sort_reverse,
savedView.filter_rules,
@ -204,11 +221,120 @@ 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', () => {
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
component.clickTag({ id: 11, name: 'Tag11' }, new MouseEvent('click'))
component.clickTag(11, new MouseEvent('click'))
expect(qfSpy).toHaveBeenCalledWith([
{ 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 get correct column title', () => {
expect(component.getColumnTitle(DashboardViewTableColumn.TITLE)).toEqual(
'Title'
)
expect(component.getColumnTitle(DashboardViewTableColumn.CREATED)).toEqual(
'Created'
)
expect(component.getColumnTitle(DashboardViewTableColumn.ADDED)).toEqual(
'Added'
)
expect(component.getColumnTitle(DashboardViewTableColumn.TAGS)).toEqual(
'Tags'
)
expect(
component.getColumnTitle(DashboardViewTableColumn.CORRESPONDENT)
).toEqual('Correspondent')
expect(
component.getColumnTitle(DashboardViewTableColumn.DOCUMENT_TYPE)
).toEqual('Document type')
expect(
component.getColumnTitle(DashboardViewTableColumn.STORAGE_PATH)
).toEqual('Storage path')
})
it('should check if column is visible including permissions', () => {
expect(
component.columnIsVisible(DashboardViewTableColumn.TITLE)
).toBeTruthy()
expect(
component.columnIsVisible(DashboardViewTableColumn.CREATED)
).toBeTruthy()
expect(
component.columnIsVisible(DashboardViewTableColumn.ADDED)
).toBeTruthy()
expect(
component.columnIsVisible(DashboardViewTableColumn.TAGS)
).toBeTruthy()
expect(
component.columnIsVisible(DashboardViewTableColumn.CORRESPONDENT)
).toBeTruthy()
expect(
component.columnIsVisible(DashboardViewTableColumn.DOCUMENT_TYPE)
).toBeTruthy()
expect(
component.columnIsVisible(DashboardViewTableColumn.STORAGE_PATH)
).toBeTruthy()
jest
.spyOn(component.permissionsService, 'currentUserCan')
.mockReturnValue(false)
expect(
component.columnIsVisible(DashboardViewTableColumn.TITLE)
).toBeTruthy()
expect(
component.columnIsVisible(DashboardViewTableColumn.CREATED)
).toBeTruthy()
expect(
component.columnIsVisible(DashboardViewTableColumn.ADDED)
).toBeTruthy()
expect(component.columnIsVisible(DashboardViewTableColumn.TAGS)).toBeFalsy()
expect(
component.columnIsVisible(DashboardViewTableColumn.CORRESPONDENT)
).toBeFalsy()
expect(
component.columnIsVisible(DashboardViewTableColumn.DOCUMENT_TYPE)
).toBeFalsy()
expect(
component.columnIsVisible(DashboardViewTableColumn.STORAGE_PATH)
).toBeFalsy()
expect(
component.columnIsVisible('unknown' as DashboardViewTableColumn)
).toBeFalsy() // coverage
})
})

View File

@ -6,23 +6,31 @@ import {
QueryList,
ViewChildren,
} from '@angular/core'
import { Params, Router } from '@angular/router'
import { Router } from '@angular/router'
import { Subject, takeUntil } from 'rxjs'
import { Document } from 'src/app/data/document'
import { SavedView } from 'src/app/data/saved-view'
import {
DashboardViewTableColumn,
DashboardViewMode,
SavedView,
} from 'src/app/data/saved-view'
import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
import { DocumentService } from 'src/app/services/rest/document.service'
import { Tag } from 'src/app/data/tag'
import {
FILTER_CORRESPONDENT,
FILTER_DOCUMENT_TYPE,
FILTER_HAS_TAGS_ALL,
FILTER_STORAGE_PATH,
} from 'src/app/data/filter-rule-type'
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import { ComponentWithPermissions } from 'src/app/components/with-permissions/with-permissions.component'
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
import { queryParamsFromFilterRules } from 'src/app/utils/query-params'
import { PermissionsService } from 'src/app/services/permissions.service'
import {
PermissionAction,
PermissionType,
PermissionsService,
} from 'src/app/services/permissions.service'
@Component({
selector: 'pngx-saved-view-widget',
@ -33,6 +41,9 @@ export class SavedViewWidgetComponent
extends ComponentWithPermissions
implements OnInit, OnDestroy
{
public DashboardViewMode = DashboardViewMode
public DashboardViewTableColumn = DashboardViewTableColumn
loading: boolean = true
constructor(
@ -80,7 +91,7 @@ export class SavedViewWidgetComponent
this.documentService
.listFiltered(
1,
10,
this.savedView.dashboard_view_limit,
this.savedView.sort_field,
this.savedView.sort_reverse,
this.savedView.filter_rules,
@ -103,15 +114,46 @@ export class SavedViewWidgetComponent
}
}
clickTag(tag: Tag, event: MouseEvent) {
event.preventDefault()
event.stopImmediatePropagation()
clickTag(tagID: number, event: MouseEvent = null) {
event?.preventDefault()
event?.stopImmediatePropagation()
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() },
])
}
openDocumentDetail(document: Document) {
this.router.navigate(['documents', document.id])
}
getPreviewUrl(document: Document): string {
return this.documentService.getPreviewUrl(document.id)
}
@ -161,14 +203,41 @@ export class SavedViewWidgetComponent
}, 300)
}
getCorrespondentQueryParams(correspondentId: number): Params {
return correspondentId !== undefined
? queryParamsFromFilterRules([
{
rule_type: FILTER_CORRESPONDENT,
value: correspondentId.toString(),
},
])
: null
public columnIsVisible(column: DashboardViewTableColumn): boolean {
if (
[
DashboardViewTableColumn.TITLE,
DashboardViewTableColumn.CREATED,
DashboardViewTableColumn.ADDED,
].includes(column)
) {
return true
} else {
const type: PermissionType = Object.values(PermissionType).find((t) =>
t.includes(column)
)
return type
? this.permissionsService.currentUserCan(PermissionAction.View, type)
: false
}
}
public getColumnTitle(column: DashboardViewTableColumn): string {
switch (column) {
case DashboardViewTableColumn.TITLE:
return $localize`Title`
case DashboardViewTableColumn.CREATED:
return $localize`Created`
case DashboardViewTableColumn.ADDED:
return $localize`Added`
case DashboardViewTableColumn.TAGS:
return $localize`Tags`
case DashboardViewTableColumn.CORRESPONDENT:
return $localize`Correspondent`
case DashboardViewTableColumn.DOCUMENT_TYPE:
return $localize`Document type`
case DashboardViewTableColumn.STORAGE_PATH:
return $localize`Storage path`
}
}
}

View File

@ -1,6 +1,21 @@
import { FilterRule } from './filter-rule'
import { ObjectWithPermissions } from './object-with-permissions'
export enum DashboardViewMode {
TABLE = 'table',
SMALL_CARDS = 'small_cards',
}
export enum DashboardViewTableColumn {
TITLE = 'title',
CREATED = 'created',
ADDED = 'added',
TAGS = 'tag',
CORRESPONDENT = 'correspondent',
DOCUMENT_TYPE = 'documenttype',
STORAGE_PATH = 'storagepath',
}
export interface SavedView extends ObjectWithPermissions {
name?: string
@ -13,4 +28,10 @@ export interface SavedView extends ObjectWithPermissions {
sort_reverse: boolean
filter_rules: FilterRule[]
dashboard_view_limit?: number
dashboard_view_mode?: DashboardViewMode
dashboard_view_table_columns?: DashboardViewTableColumn[]
}

View File

@ -0,0 +1,52 @@
# Generated by Django 4.2.11 on 2024-04-16 18:35
import django.core.validators
import multiselectfield.db.fields
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="dashboard_view_mode",
field=models.CharField(
choices=[("table", "Table"), ("small_cards", "Small Cards")],
default="table",
max_length=128,
verbose_name="Dashboard view display mode",
),
),
migrations.AddField(
model_name="savedview",
name="dashboard_view_limit",
field=models.PositiveIntegerField(
default=10,
validators=[django.core.validators.MinValueValidator(1)],
verbose_name="Dashboard view limit",
),
),
migrations.AddField(
model_name="savedview",
name="dashboard_view_table_columns",
field=multiselectfield.db.fields.MultiSelectField(
choices=[
("title", "Title"),
("created", "Created"),
("added", "Added"),
("tag", "Tags"),
("documenttype", "Document Type"),
("correspondent", "Correspondent"),
("storagepath", "Storage Path"),
],
default="created,title,tags,correspondent",
max_length=128,
),
),
]

View File

@ -394,6 +394,19 @@ class Log(models.Model):
class SavedView(ModelWithOwner):
class DashboardViewDisplayMode(models.TextChoices):
TABLE = ("table", _("Table"))
SMALL_CARDS = ("small_cards", _("Small Cards"))
class DashboardViewTableColumns(models.TextChoices):
TITLE = ("title", _("Title"))
CREATED = ("created", _("Created"))
ADDED = ("added", _("Added"))
TAGS = ("tag"), _("Tags")
DOCUMENT_TYPE = ("documenttype", _("Document Type"))
CORRESPONDENT = ("correspondent", _("Correspondent"))
STORAGE_PATH = ("storagepath", _("Storage Path"))
name = models.CharField(_("name"), max_length=128)
show_on_dashboard = models.BooleanField(
@ -411,6 +424,26 @@ class SavedView(ModelWithOwner):
)
sort_reverse = models.BooleanField(_("sort reverse"), default=False)
dashboard_view_limit = models.PositiveIntegerField(
_("Dashboard view limit"),
default=10,
validators=[MinValueValidator(1)],
)
dashboard_view_mode = models.CharField(
max_length=128,
verbose_name=_("Dashboard view display mode"),
choices=DashboardViewDisplayMode.choices,
default=DashboardViewDisplayMode.TABLE,
)
dashboard_view_table_columns = MultiSelectField(
max_length=128,
verbose_name=_("Dashboard view table display columns"),
choices=DashboardViewTableColumns.choices,
default=f"{DashboardViewTableColumns.CREATED},{DashboardViewTableColumns.TITLE},{DashboardViewTableColumns.TAGS},{DashboardViewTableColumns.CORRESPONDENT}",
)
class Meta:
ordering = ("name",)
verbose_name = _("saved view")

View File

@ -799,6 +799,10 @@ class SavedViewFilterRuleSerializer(serializers.ModelSerializer):
class SavedViewSerializer(OwnedObjectSerializer):
filter_rules = SavedViewFilterRuleSerializer(many=True)
dashboard_view_table_columns = fields.MultipleChoiceField(
choices=SavedView.DashboardViewTableColumns.choices,
required=False,
)
class Meta:
model = SavedView
@ -810,6 +814,9 @@ class SavedViewSerializer(OwnedObjectSerializer):
"sort_field",
"sort_reverse",
"filter_rules",
"dashboard_view_limit",
"dashboard_view_mode",
"dashboard_view_table_columns",
"owner",
"permissions",
"user_can_change",