Prepare explorer page
This commit is contained in:
parent
5f33ff941e
commit
62d6c30ee1
@ -1,22 +1,23 @@
|
|||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { Routes, RouterModule } from '@angular/router'
|
import { RouterModule, Routes } from '@angular/router'
|
||||||
import { AppFrameComponent } from './components/app-frame/app-frame.component'
|
import { AppFrameComponent } from './components/app-frame/app-frame.component'
|
||||||
import { DashboardComponent } from './components/dashboard/dashboard.component'
|
import { DashboardComponent } from './components/dashboard/dashboard.component'
|
||||||
|
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
||||||
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
|
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
|
||||||
import { DocumentListComponent } from './components/document-list/document-list.component'
|
import { DocumentListComponent } from './components/document-list/document-list.component'
|
||||||
|
import { ExplorerComponent } from './components/explorer/explorer.component'
|
||||||
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'
|
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'
|
||||||
import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'
|
import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'
|
||||||
import { LogsComponent } from './components/manage/logs/logs.component'
|
import { LogsComponent } from './components/manage/logs/logs.component'
|
||||||
import { SettingsComponent } from './components/manage/settings/settings.component'
|
import { SettingsComponent } from './components/manage/settings/settings.component'
|
||||||
import { TagListComponent } from './components/manage/tag-list/tag-list.component'
|
|
||||||
import { NotFoundComponent } from './components/not-found/not-found.component'
|
|
||||||
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
|
||||||
import { DirtyFormGuard } from './guards/dirty-form.guard'
|
|
||||||
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
|
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
|
||||||
|
import { TagListComponent } from './components/manage/tag-list/tag-list.component'
|
||||||
import { TasksComponent } from './components/manage/tasks/tasks.component'
|
import { TasksComponent } from './components/manage/tasks/tasks.component'
|
||||||
import { PermissionsGuard } from './guards/permissions.guard'
|
import { NotFoundComponent } from './components/not-found/not-found.component'
|
||||||
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
||||||
|
import { DirtyFormGuard } from './guards/dirty-form.guard'
|
||||||
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
|
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
|
||||||
|
import { PermissionsGuard } from './guards/permissions.guard'
|
||||||
import {
|
import {
|
||||||
PermissionAction,
|
PermissionAction,
|
||||||
PermissionType,
|
PermissionType,
|
||||||
@ -30,6 +31,18 @@ const routes: Routes = [
|
|||||||
canDeactivate: [DirtyDocGuard],
|
canDeactivate: [DirtyDocGuard],
|
||||||
children: [
|
children: [
|
||||||
{ path: 'dashboard', component: DashboardComponent },
|
{ path: 'dashboard', component: DashboardComponent },
|
||||||
|
{
|
||||||
|
path: 'explorer',
|
||||||
|
component: ExplorerComponent,
|
||||||
|
canDeactivate: [DirtySavedViewGuard],
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.Document,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'documents',
|
path: 'documents',
|
||||||
component: DocumentListComponent,
|
component: DocumentListComponent,
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'
|
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'
|
||||||
import { DocumentListComponent } from './components/document-list/document-list.component'
|
import { DocumentListComponent } from './components/document-list/document-list.component'
|
||||||
|
import { ExplorerComponent } from './components/explorer/explorer.component'
|
||||||
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
|
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
|
||||||
import { DashboardComponent } from './components/dashboard/dashboard.component'
|
import { DashboardComponent } from './components/dashboard/dashboard.component'
|
||||||
import { TagListComponent } from './components/manage/tag-list/tag-list.component'
|
import { TagListComponent } from './components/manage/tag-list/tag-list.component'
|
||||||
@ -29,6 +30,7 @@ import { PageHeaderComponent } from './components/common/page-header/page-header
|
|||||||
import { AppFrameComponent } from './components/app-frame/app-frame.component'
|
import { AppFrameComponent } from './components/app-frame/app-frame.component'
|
||||||
import { ToastsComponent } from './components/common/toasts/toasts.component'
|
import { ToastsComponent } from './components/common/toasts/toasts.component'
|
||||||
import { FilterEditorComponent } from './components/document-list/filter-editor/filter-editor.component'
|
import { FilterEditorComponent } from './components/document-list/filter-editor/filter-editor.component'
|
||||||
|
import { FilterEditorComponent as ExplorerFilterEditorComponent } from './components/explorer/filter-editor/filter-editor.component'
|
||||||
import { FilterableDropdownComponent } from './components/common/filterable-dropdown/filterable-dropdown.component'
|
import { FilterableDropdownComponent } from './components/common/filterable-dropdown/filterable-dropdown.component'
|
||||||
import { ToggleableDropdownButtonComponent } from './components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component'
|
import { ToggleableDropdownButtonComponent } from './components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component'
|
||||||
import { DateDropdownComponent } from './components/common/date-dropdown/date-dropdown.component'
|
import { DateDropdownComponent } from './components/common/date-dropdown/date-dropdown.component'
|
||||||
@ -144,6 +146,7 @@ function initializeApp(settings: SettingsService) {
|
|||||||
declarations: [
|
declarations: [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
DocumentListComponent,
|
DocumentListComponent,
|
||||||
|
ExplorerComponent,
|
||||||
DocumentDetailComponent,
|
DocumentDetailComponent,
|
||||||
DashboardComponent,
|
DashboardComponent,
|
||||||
TagListComponent,
|
TagListComponent,
|
||||||
@ -164,6 +167,7 @@ function initializeApp(settings: SettingsService) {
|
|||||||
AppFrameComponent,
|
AppFrameComponent,
|
||||||
ToastsComponent,
|
ToastsComponent,
|
||||||
FilterEditorComponent,
|
FilterEditorComponent,
|
||||||
|
ExplorerFilterEditorComponent,
|
||||||
FilterableDropdownComponent,
|
FilterableDropdownComponent,
|
||||||
ToggleableDropdownButtonComponent,
|
ToggleableDropdownButtonComponent,
|
||||||
DateDropdownComponent,
|
DateDropdownComponent,
|
||||||
|
@ -72,9 +72,16 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||||
<a class="nav-link" routerLink="documents" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Documents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
<a class="nav-link" routerLink="documents" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Documents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#search"/>
|
||||||
|
</svg><span> <ng-container i18n>Search</ng-container></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" routerLink="explorer" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Explorer" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#files"/>
|
<use xlink:href="assets/bootstrap-icons.svg#files"/>
|
||||||
</svg><span> <ng-container i18n>Documents</ng-container></span>
|
</svg><span> <ng-container i18n>File Explorer</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
233
src-ui/src/app/components/explorer/explorer.component.html
Normal file
233
src-ui/src/app/components/explorer/explorer.component.html
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
<app-page-header [title]="getTitle()">
|
||||||
|
|
||||||
|
<div ngbDropdown class="me-2 d-flex">
|
||||||
|
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
|
||||||
|
<svg class="toolbaricon" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#text-indent-left" />
|
||||||
|
</svg>
|
||||||
|
<div class="d-none d-sm-inline"> <ng-container i18n>Select</ng-container></div>
|
||||||
|
</button>
|
||||||
|
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
|
||||||
|
<button ngbDropdownItem (click)="list.selectNone()" i18n>Select none</button>
|
||||||
|
<button ngbDropdownItem (click)="list.selectPage()" i18n>Select page</button>
|
||||||
|
<button ngbDropdownItem (click)="list.selectAll()" i18n>Select all</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group flex-fill" role="group">
|
||||||
|
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="details" (ngModelChange)="saveDisplayMode()" id="displayModeDetails" name="displayModeDetails">
|
||||||
|
<label for="displayModeDetails" class="btn btn-outline-primary btn-sm">
|
||||||
|
<svg class="toolbaricon" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#list-ul" />
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
|
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="smallCards" (ngModelChange)="saveDisplayMode()" id="displayModeSmall" name="displayModeSmall">
|
||||||
|
<label for="displayModeSmall" class="btn btn-outline-primary btn-sm">
|
||||||
|
<svg class="toolbaricon" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#grid" />
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
|
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="largeCards" (ngModelChange)="saveDisplayMode()" id="displayModeLarge" name="displayModeLarge">
|
||||||
|
<label for="displayModeLarge" class="btn btn-outline-primary btn-sm">
|
||||||
|
<svg class="toolbaricon" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#hdd-stack" />
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ngbDropdown class="btn-group ms-2 flex-fill">
|
||||||
|
<button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle i18n>Sort</button>
|
||||||
|
<div ngbDropdownMenu aria-labelledby="dropdownBasic1" class="shadow dropdown-menu-right">
|
||||||
|
<div class="w-100 d-flex pb-2 mb-1 border-bottom">
|
||||||
|
<input type="radio" class="btn-check" [value]="false" [(ngModel)]="listSortReverse" id="listSortReverseFalse">
|
||||||
|
<label class="btn btn-outline-primary btn-sm mx-2 flex-fill" for="listSortReverseFalse">
|
||||||
|
<svg class="toolbaricon" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#sort-alpha-down" />
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
|
<input type="radio" class="btn-check" [value]="true" [(ngModel)]="listSortReverse" id="listSortReverseTrue">
|
||||||
|
<label class="btn btn-outline-primary btn-sm me-2 flex-fill" for="listSortReverseTrue">
|
||||||
|
<svg class="toolbaricon" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#sort-alpha-up-alt" />
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button *ngFor="let f of getSortFields()" ngbDropdownItem (click)="setSortField(f.field)"
|
||||||
|
[class.active]="list.sortField === f.field">{{f.name}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-group ms-2 flex-fill" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }" ngbDropdown role="group">
|
||||||
|
<button class="btn btn-sm btn-outline-primary dropdown-toggle flex-fill" tourAnchor="tour.documents-views" ngbDropdownToggle>
|
||||||
|
<ng-container i18n>Views</ng-container>
|
||||||
|
<div *ngIf="savedViewIsModified" class="position-absolute top-0 start-100 p-2 translate-middle badge bg-secondary border border-light rounded-circle">
|
||||||
|
<span class="visually-hidden">selected</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu shadow dropdown-menu-right" ngbDropdownMenu>
|
||||||
|
<ng-container *ngIf="!list.activeSavedViewId">
|
||||||
|
<button ngbDropdownItem *ngFor="let view of savedViewService.allViews" (click)="loadViewConfig(view.id)">{{view.name}}</button>
|
||||||
|
<div class="dropdown-divider" *ngIf="savedViewService.allViews.length > 0"></div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</app-page-header>
|
||||||
|
|
||||||
|
<div class="row sticky-top pt-3 pt-sm-4 pb-2 pb-lg-4 bg-body">
|
||||||
|
<app-filter-editor [hidden]="isBulkEditing" [(filterRules)]="list.filterRules" [unmodifiedFilterRules]="unmodifiedFilterRules" [selectionData]="list.selectionData" #filterEditor></app-filter-editor>
|
||||||
|
<app-bulk-editor [hidden]="!isBulkEditing"></app-bulk-editor>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<ng-template #pagination>
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<p>
|
||||||
|
<ng-container *ngIf="list.isReloading">
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
|
<ng-container i18n>Loading...</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
<span i18n *ngIf="list.selected.size > 0">{list.collectionSize, plural, =1 {Selected {{list.selected.size}} of one document} other {Selected {{list.selected.size}} of {{list.collectionSize || 0}} documents}}</span>
|
||||||
|
<ng-container *ngIf="!list.isReloading">
|
||||||
|
<span i18n *ngIf="list.selected.size === 0">{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</span> <span i18n *ngIf="isFiltered">(filtered)</span>
|
||||||
|
</ng-container>
|
||||||
|
</p>
|
||||||
|
<ngb-pagination *ngIf="list.collectionSize" [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
|
||||||
|
[rotate]="true" aria-label="Default pagination"></ngb-pagination>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<div tourAnchor="tour.documents">
|
||||||
|
<ng-container *ngTemplateOutlet="pagination"></ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-container *ngIf="list.error ; else documentListNoError">
|
||||||
|
<div class="alert alert-danger" role="alert"><ng-container i18n>Error while loading documents</ng-container>: {{list.error}}</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-template #documentListNoError>
|
||||||
|
<div *ngIf="displayMode === 'largeCards'">
|
||||||
|
<app-document-card-large [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" (dblClickDocument)="openDocumentDetail(d)" *ngFor="let d of list.documents; trackBy: trackByDocumentId" [document]="d" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickDocumentType)="clickDocumentType($event)" (clickStoragePath)="clickStoragePath($event)" (clickMoreLike)="clickMoreLike(d.id)">
|
||||||
|
</app-document-card-large>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="table table-sm align-middle border shadow-sm" *ngIf="displayMode === 'details'">
|
||||||
|
<thead>
|
||||||
|
<th></th>
|
||||||
|
<th class="d-none d-lg-table-cell"
|
||||||
|
appSortable="archive_serial_number"
|
||||||
|
title="Sort by ASN" i18n-title
|
||||||
|
[currentSortField]="list.sortField"
|
||||||
|
[currentSortReverse]="list.sortReverse"
|
||||||
|
(sort)="onSort($event)"
|
||||||
|
i18n>ASN</th>
|
||||||
|
<th class="d-none d-md-table-cell"
|
||||||
|
appSortable="correspondent__name"
|
||||||
|
title="Sort by correspondent" i18n-title
|
||||||
|
[currentSortField]="list.sortField"
|
||||||
|
[currentSortReverse]="list.sortReverse"
|
||||||
|
(sort)="onSort($event)"
|
||||||
|
i18n>Correspondent</th>
|
||||||
|
<th
|
||||||
|
appSortable="title"
|
||||||
|
title="Sort by title" i18n-title
|
||||||
|
[currentSortField]="list.sortField"
|
||||||
|
[currentSortReverse]="list.sortReverse"
|
||||||
|
(sort)="onSort($event)"
|
||||||
|
i18n>Title</th>
|
||||||
|
<th *ngIf="notesEnabled" class="d-none d-xl-table-cell"
|
||||||
|
appSortable="num_notes"
|
||||||
|
title="Sort by notes" i18n-title
|
||||||
|
[currentSortField]="list.sortField"
|
||||||
|
[currentSortReverse]="list.sortReverse"
|
||||||
|
(sort)="onSort($event)"
|
||||||
|
i18n>Notes</th>
|
||||||
|
<th class="d-none d-xl-table-cell"
|
||||||
|
appSortable="document_type__name"
|
||||||
|
title="Sort by document type" i18n-title
|
||||||
|
[currentSortField]="list.sortField"
|
||||||
|
[currentSortReverse]="list.sortReverse"
|
||||||
|
(sort)="onSort($event)"
|
||||||
|
i18n>Document type</th>
|
||||||
|
<th class="d-none d-xl-table-cell"
|
||||||
|
appSortable="storage_path__name"
|
||||||
|
title="Sort by storage path" i18n-title
|
||||||
|
[currentSortField]="list.sortField"
|
||||||
|
[currentSortReverse]="list.sortReverse"
|
||||||
|
(sort)="onSort($event)"
|
||||||
|
i18n>Storage path</th>
|
||||||
|
<th
|
||||||
|
appSortable="created"
|
||||||
|
title="Sort by created date" i18n-title
|
||||||
|
[currentSortField]="list.sortField"
|
||||||
|
[currentSortReverse]="list.sortReverse"
|
||||||
|
(sort)="onSort($event)"
|
||||||
|
i18n>Created</th>
|
||||||
|
<th class="d-none d-xl-table-cell"
|
||||||
|
appSortable="added"
|
||||||
|
title="Sort by added date" i18n-title
|
||||||
|
[currentSortField]="list.sortField"
|
||||||
|
[currentSortReverse]="list.sortReverse"
|
||||||
|
(sort)="onSort($event)"
|
||||||
|
i18n>Added</th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let d of list.documents; trackBy: trackByDocumentId" (click)="toggleSelected(d, $event); $event.stopPropagation();" (dblclick)="openDocumentDetail(d)" [ngClass]="list.isSelected(d) ? 'table-row-selected' : ''">
|
||||||
|
<td>
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="docCheck{{d.id}}" [checked]="list.isSelected(d)" (click)="toggleSelected(d, $event); $event.stopPropagation();">
|
||||||
|
<label class="form-check-label" for="docCheck{{d.id}}"></label>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="d-none d-lg-table-cell">
|
||||||
|
{{d.archive_serial_number}}
|
||||||
|
</td>
|
||||||
|
<td class="d-none d-md-table-cell">
|
||||||
|
<ng-container *ngIf="d.correspondent">
|
||||||
|
<a (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent" i18n-title>{{(d.correspondent$ | async)?.name}}</a>
|
||||||
|
</ng-container>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
|
||||||
|
<app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ms-1" clickable="true" linkTitle="Filter by tag" i18n-linkTitle (click)="clickTag(t.id);$event.stopPropagation()"></app-tag>
|
||||||
|
</td>
|
||||||
|
<td *ngIf="notesEnabled" class="d-none d-xl-table-cell">
|
||||||
|
<a *ngIf="d.notes.length" routerLink="/documents/{{d.id}}/notes" class="btn btn-sm p-0">
|
||||||
|
<span class="badge rounded-pill bg-light border text-primary">
|
||||||
|
<svg class="metadata-icon ms-1 me-1" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/>
|
||||||
|
</svg>
|
||||||
|
{{d.notes.length}}</span>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="d-none d-xl-table-cell">
|
||||||
|
<ng-container *ngIf="d.document_type">
|
||||||
|
<a (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type" i18n-title>{{(d.document_type$ | async)?.name}}</a>
|
||||||
|
</ng-container>
|
||||||
|
</td>
|
||||||
|
<td class="d-none d-xl-table-cell">
|
||||||
|
<ng-container *ngIf="d.storage_path">
|
||||||
|
<a (click)="clickStoragePath(d.storage_path);$event.stopPropagation()" title="Filter by storage path" i18n-title>{{(d.storage_path$ | async)?.name}}</a>
|
||||||
|
</ng-container>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{d.created_date | customDate}}
|
||||||
|
</td>
|
||||||
|
<td class="d-none d-xl-table-cell">
|
||||||
|
{{d.added | customDate}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="row row-cols-paperless-cards" *ngIf="displayMode === 'smallCards'">
|
||||||
|
<app-document-card-small class="p-0" [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" (dblClickDocument)="openDocumentDetail(d)" [document]="d" *ngFor="let d of list.documents; trackBy: trackByDocumentId" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickStoragePath)="clickStoragePath($event)" (clickDocumentType)="clickDocumentType($event)"></app-document-card-small>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="list.documents?.length > 15" class="mt-3">
|
||||||
|
<ng-container *ngTemplateOutlet="pagination"></ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</ng-template>
|
82
src-ui/src/app/components/explorer/explorer.component.scss
Normal file
82
src-ui/src/app/components/explorer/explorer.component.scss
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
::ng-deep app-document-list app-page-header > div.mb-3 {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row-selected {
|
||||||
|
background-color: var(--pngx-primary-faded);
|
||||||
|
}
|
||||||
|
|
||||||
|
$paperless-card-breakpoints: (
|
||||||
|
// 0: 2, // xs is manual for slim-sidebar
|
||||||
|
768px: 3, //md
|
||||||
|
992px: 4, //lg
|
||||||
|
1200px: 5, //xl
|
||||||
|
1400px: 6, // xxl
|
||||||
|
1600px: 7,
|
||||||
|
1800px: 8,
|
||||||
|
2000px: 9
|
||||||
|
);
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu-right {
|
||||||
|
right: 0 !important;
|
||||||
|
left: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sticky-top {
|
||||||
|
z-index: 990; // below main navbar
|
||||||
|
top: calc(7rem - 2px); // height of navbar (mobile)
|
||||||
|
|
||||||
|
@media (min-width: 580px) {
|
||||||
|
top: 3.5rem; // height of navbar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table .form-check {
|
||||||
|
padding: 0.2rem;
|
||||||
|
min-height: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
.form-check-input {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
243
src-ui/src/app/components/explorer/explorer.component.ts
Normal file
243
src-ui/src/app/components/explorer/explorer.component.ts
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
QueryList,
|
||||||
|
ViewChild,
|
||||||
|
ViewChildren,
|
||||||
|
} from '@angular/core'
|
||||||
|
import { ActivatedRoute, convertToParamMap, Router } from '@angular/router'
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { filter, first, map, Subject, switchMap, takeUntil } from 'rxjs'
|
||||||
|
import {
|
||||||
|
FilterRule,
|
||||||
|
filterRulesDiffer,
|
||||||
|
isFullTextFilterRule,
|
||||||
|
} from 'src/app/data/filter-rule'
|
||||||
|
import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type'
|
||||||
|
import { PaperlessDocument } from 'src/app/data/paperless-document'
|
||||||
|
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
|
||||||
|
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||||
|
import {
|
||||||
|
SortableDirective,
|
||||||
|
SortEvent,
|
||||||
|
} from 'src/app/directives/sortable.directive'
|
||||||
|
import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
|
||||||
|
import { DocumentListViewService } from 'src/app/services/document-list-view.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 { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||||
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
||||||
|
import { FilterEditorComponent } from './filter-editor/filter-editor.component'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-explorer',
|
||||||
|
templateUrl: './explorer.component.html',
|
||||||
|
styleUrls: ['./explorer.component.scss'],
|
||||||
|
})
|
||||||
|
export class ExplorerComponent
|
||||||
|
extends ComponentWithPermissions
|
||||||
|
implements OnInit, OnDestroy
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
public list: DocumentListViewService,
|
||||||
|
public savedViewService: SavedViewService,
|
||||||
|
public route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
|
private toastService: ToastService,
|
||||||
|
private modalService: NgbModal,
|
||||||
|
private consumerStatusService: ConsumerStatusService,
|
||||||
|
public openDocumentsService: OpenDocumentsService,
|
||||||
|
private settingsService: SettingsService
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewChild('filterEditor')
|
||||||
|
private filterEditor: FilterEditorComponent
|
||||||
|
|
||||||
|
@ViewChildren(SortableDirective) headers: QueryList<SortableDirective>
|
||||||
|
|
||||||
|
displayMode = 'smallCards' // largeCards, smallCards, details
|
||||||
|
|
||||||
|
unmodifiedFilterRules: FilterRule[] = []
|
||||||
|
private unmodifiedSavedView: PaperlessSavedView
|
||||||
|
|
||||||
|
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||||
|
|
||||||
|
get savedViewIsModified(): boolean {
|
||||||
|
if (!this.list.activeSavedViewId || !this.unmodifiedSavedView) return false
|
||||||
|
else {
|
||||||
|
return (
|
||||||
|
this.unmodifiedSavedView.sort_field !== this.list.sortField ||
|
||||||
|
this.unmodifiedSavedView.sort_reverse !== this.list.sortReverse ||
|
||||||
|
filterRulesDiffer(
|
||||||
|
this.unmodifiedSavedView.filter_rules,
|
||||||
|
this.list.filterRules
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get isFiltered() {
|
||||||
|
return this.list.filterRules?.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
getTitle() {
|
||||||
|
let title = this.list.activeSavedViewTitle
|
||||||
|
if (title && this.savedViewIsModified) {
|
||||||
|
title += '*'
|
||||||
|
} else if (!title) {
|
||||||
|
title = $localize`File Explorer`
|
||||||
|
}
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
|
||||||
|
getSortFields() {
|
||||||
|
return isFullTextFilterRule(this.list.filterRules)
|
||||||
|
? DOCUMENT_SORT_FIELDS_FULLTEXT
|
||||||
|
: DOCUMENT_SORT_FIELDS
|
||||||
|
}
|
||||||
|
|
||||||
|
set listSortReverse(reverse: boolean) {
|
||||||
|
this.list.sortReverse = reverse
|
||||||
|
}
|
||||||
|
|
||||||
|
get listSortReverse(): boolean {
|
||||||
|
return this.list.sortReverse
|
||||||
|
}
|
||||||
|
|
||||||
|
setSortField(field: string) {
|
||||||
|
this.list.sortField = field
|
||||||
|
}
|
||||||
|
|
||||||
|
onSort(event: SortEvent) {
|
||||||
|
this.list.setSort(event.column, event.reverse)
|
||||||
|
}
|
||||||
|
|
||||||
|
get isBulkEditing(): boolean {
|
||||||
|
return this.list.selected.size > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
saveDisplayMode() {
|
||||||
|
localStorage.setItem('document-list:displayMode', this.displayMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
if (localStorage.getItem('document-list:displayMode') != null) {
|
||||||
|
this.displayMode = localStorage.getItem('document-list:displayMode')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.consumerStatusService
|
||||||
|
.onDocumentConsumptionFinished()
|
||||||
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.list.reload()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.route.paramMap
|
||||||
|
.pipe(
|
||||||
|
filter((params) => params.has('id')), // only on saved view e.g. /view/id
|
||||||
|
switchMap((params) => {
|
||||||
|
return this.savedViewService
|
||||||
|
.getCached(+params.get('id'))
|
||||||
|
.pipe(map((view) => ({ view })))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe(({ view }) => {
|
||||||
|
if (!view) {
|
||||||
|
this.router.navigate(['404'])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.unmodifiedSavedView = view
|
||||||
|
this.list.activateSavedViewWithQueryParams(
|
||||||
|
view,
|
||||||
|
convertToParamMap(this.route.snapshot.queryParams)
|
||||||
|
)
|
||||||
|
// this.list.reload()
|
||||||
|
this.unmodifiedFilterRules = view.filter_rules
|
||||||
|
})
|
||||||
|
|
||||||
|
this.route.queryParamMap
|
||||||
|
.pipe(
|
||||||
|
filter(() => !this.route.snapshot.paramMap.has('id')), // only when not on /view/id
|
||||||
|
takeUntil(this.unsubscribeNotifier)
|
||||||
|
)
|
||||||
|
.subscribe((queryParams) => {
|
||||||
|
if (queryParams.has('view')) {
|
||||||
|
// loading a saved view on /documents
|
||||||
|
this.loadViewConfig(parseInt(queryParams.get('view')))
|
||||||
|
} else {
|
||||||
|
// this.list.activateSavedView(null)
|
||||||
|
// this.list.loadFromQueryParams(queryParams)
|
||||||
|
// this.unmodifiedFilterRules = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
// unsubscribes all
|
||||||
|
this.unsubscribeNotifier.next(this)
|
||||||
|
this.unsubscribeNotifier.complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
loadViewConfig(viewID: number) {
|
||||||
|
this.savedViewService
|
||||||
|
.getCached(viewID)
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe((view) => {
|
||||||
|
this.unmodifiedSavedView = view
|
||||||
|
this.list.activateSavedView(view)
|
||||||
|
this.list.reload()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
openDocumentDetail(document: PaperlessDocument) {
|
||||||
|
this.router.navigate(['documents', document.id])
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSelected(document: PaperlessDocument, event: MouseEvent): void {
|
||||||
|
if (!event.shiftKey) this.list.toggleSelected(document)
|
||||||
|
else this.list.selectRangeTo(document)
|
||||||
|
}
|
||||||
|
|
||||||
|
clickTag(tagID: number) {
|
||||||
|
this.list.selectNone()
|
||||||
|
this.filterEditor.toggleTag(tagID)
|
||||||
|
}
|
||||||
|
|
||||||
|
clickCorrespondent(correspondentID: number) {
|
||||||
|
this.list.selectNone()
|
||||||
|
this.filterEditor.toggleCorrespondent(correspondentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
clickDocumentType(documentTypeID: number) {
|
||||||
|
this.list.selectNone()
|
||||||
|
this.filterEditor.toggleDocumentType(documentTypeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
clickStoragePath(storagePathID: number) {
|
||||||
|
this.list.selectNone()
|
||||||
|
this.filterEditor.toggleStoragePath(storagePathID)
|
||||||
|
}
|
||||||
|
|
||||||
|
clickMoreLike(documentID: number) {
|
||||||
|
this.list.quickFilter([
|
||||||
|
{ rule_type: FILTER_FULLTEXT_MORELIKE, value: documentID.toString() },
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
trackByDocumentId(index, item: PaperlessDocument) {
|
||||||
|
return item.id
|
||||||
|
}
|
||||||
|
|
||||||
|
get notesEnabled(): boolean {
|
||||||
|
return this.settingsService.get(SETTINGS_KEYS.NOTES_ENABLED)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
<div class="row flex-wrap" tourAnchor="tour.documents-filter-editor">
|
||||||
|
<div class="col mb-2 mb-xxl-0">
|
||||||
|
<div class="form-inline d-flex align-items-center">
|
||||||
|
<div class="input-group input-group-sm flex-fill w-auto flex-nowrap">
|
||||||
|
<div ngbDropdown>
|
||||||
|
<button class="btn btn-sm btn-outline-primary" ngbDropdownToggle>{{textFilterTargetName}}</button>
|
||||||
|
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
||||||
|
<button *ngFor="let t of textFilterTargets" ngbDropdownItem [class.active]="textFilterTarget === t.id" (click)="changeTextFilterTarget(t.id)">{{t.name}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<select *ngIf="textFilterTarget === 'asn'" class="form-select flex-grow-0 w-auto" [(ngModel)]="textFilterModifier" (change)="textFilterModifierChange()">
|
||||||
|
<option *ngFor="let m of textFilterModifiers" ngbDropdownItem [value]="m.id">{{m.label}}</option>
|
||||||
|
</select>
|
||||||
|
<button *ngIf="_textFilter" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0 z-10" (click)="resetTextField()">
|
||||||
|
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x me-1" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<input #textFilterInput class="form-control form-control-sm" type="text" [disabled]="textFilterModifierIsNull" [(ngModel)]="textFilter" (keyup)="textFilterKeyup($event)" [readonly]="textFilterTarget === 'fulltext-morelike'">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-100 d-xxl-none"></div>
|
||||||
|
<div class="col col-xl-auto">
|
||||||
|
<div class="d-flex flex-wrap">
|
||||||
|
<div class="d-flex flex-wrap mb-2 mb-xxl-0">
|
||||||
|
<app-filterable-dropdown class="flex-fill" title="Tags" icon="tag-fill" i18n-title
|
||||||
|
filterPlaceholder="Filter tags" i18n-filterPlaceholder
|
||||||
|
[items]="tags"
|
||||||
|
[manyToOne]="true"
|
||||||
|
[(selectionModel)]="tagSelectionModel"
|
||||||
|
(selectionModelChange)="updateRules()"
|
||||||
|
(opened)="onTagsDropdownOpen()"
|
||||||
|
[documentCounts]="tagDocumentCounts"
|
||||||
|
[allowSelectNone]="true"></app-filterable-dropdown>
|
||||||
|
<app-filterable-dropdown class="flex-fill" title="Correspondent" icon="person-fill" i18n-title
|
||||||
|
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
|
||||||
|
[items]="correspondents"
|
||||||
|
[(selectionModel)]="correspondentSelectionModel"
|
||||||
|
(selectionModelChange)="updateRules()"
|
||||||
|
(opened)="onCorrespondentDropdownOpen()"
|
||||||
|
[documentCounts]="correspondentDocumentCounts"
|
||||||
|
[allowSelectNone]="true"></app-filterable-dropdown>
|
||||||
|
<app-filterable-dropdown class="flex-fill" title="Document type" icon="file-earmark-fill" i18n-title
|
||||||
|
filterPlaceholder="Filter document types" i18n-filterPlaceholder
|
||||||
|
[items]="documentTypes"
|
||||||
|
[(selectionModel)]="documentTypeSelectionModel"
|
||||||
|
(selectionModelChange)="updateRules()"
|
||||||
|
(opened)="onDocumentTypeDropdownOpen()"
|
||||||
|
[documentCounts]="documentTypeDocumentCounts"
|
||||||
|
[allowSelectNone]="true"></app-filterable-dropdown>
|
||||||
|
<app-filterable-dropdown class="me-2 flex-fill" title="Storage path" icon="folder-fill" i18n-title
|
||||||
|
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
|
||||||
|
[items]="storagePaths"
|
||||||
|
[(selectionModel)]="storagePathSelectionModel"
|
||||||
|
(selectionModelChange)="updateRules()"
|
||||||
|
(opened)="onStoragePathDropdownOpen()"
|
||||||
|
[documentCounts]="storagePathDocumentCounts"
|
||||||
|
[allowSelectNone]="true"></app-filterable-dropdown>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-wrap">
|
||||||
|
<app-date-dropdown class="mb-2 mb-xl-0"
|
||||||
|
title="Created" i18n-title
|
||||||
|
(datesSet)="updateRules()"
|
||||||
|
[(dateBefore)]="dateCreatedBefore"
|
||||||
|
[(dateAfter)]="dateCreatedAfter"
|
||||||
|
[(relativeDate)]="dateCreatedRelativeDate"></app-date-dropdown>
|
||||||
|
<app-date-dropdown class="mb-2 mb-xl-0"
|
||||||
|
title="Added" i18n-title
|
||||||
|
(datesSet)="updateRules()"
|
||||||
|
[(dateBefore)]="dateAddedBefore"
|
||||||
|
[(dateAfter)]="dateAddedAfter"
|
||||||
|
[(relativeDate)]="dateAddedRelativeDate"></app-date-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-100 d-xxl-none"></div>
|
||||||
|
<div class="col col-xl-auto ps-xxl-0">
|
||||||
|
<button class="btn btn-link btn-sm px-0" [disabled]="!rulesModified" (click)="resetSelected()">
|
||||||
|
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x me-1 ms-n1" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
||||||
|
</svg><ng-container i18n>Reset filters</ng-container>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,27 @@
|
|||||||
|
.quick-filter {
|
||||||
|
min-width: 250px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
.selected-icon {
|
||||||
|
min-width: 1em;
|
||||||
|
min-height: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group .dropdown .btn {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-flex.flex-wrap {
|
||||||
|
column-gap: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.z-10 {
|
||||||
|
z-index: 10;
|
||||||
|
}
|
@ -0,0 +1,856 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
Output,
|
||||||
|
OnInit,
|
||||||
|
OnDestroy,
|
||||||
|
ViewChild,
|
||||||
|
ElementRef,
|
||||||
|
} from '@angular/core'
|
||||||
|
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||||
|
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
||||||
|
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
|
||||||
|
import { Subject, Subscription } from 'rxjs'
|
||||||
|
import { debounceTime, distinctUntilChanged, filter } from 'rxjs/operators'
|
||||||
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||||
|
import { TagService } from 'src/app/services/rest/tag.service'
|
||||||
|
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||||
|
import { filterRulesDiffer, FilterRule } from 'src/app/data/filter-rule'
|
||||||
|
import {
|
||||||
|
FILTER_ADDED_AFTER,
|
||||||
|
FILTER_ADDED_BEFORE,
|
||||||
|
FILTER_ASN,
|
||||||
|
FILTER_HAS_CORRESPONDENT_ANY,
|
||||||
|
FILTER_CREATED_AFTER,
|
||||||
|
FILTER_CREATED_BEFORE,
|
||||||
|
FILTER_HAS_DOCUMENT_TYPE_ANY,
|
||||||
|
FILTER_FULLTEXT_MORELIKE,
|
||||||
|
FILTER_FULLTEXT_QUERY,
|
||||||
|
FILTER_HAS_ANY_TAG,
|
||||||
|
FILTER_HAS_TAGS_ALL,
|
||||||
|
FILTER_HAS_TAGS_ANY,
|
||||||
|
FILTER_DOES_NOT_HAVE_TAG,
|
||||||
|
FILTER_TITLE,
|
||||||
|
FILTER_TITLE_CONTENT,
|
||||||
|
FILTER_HAS_STORAGE_PATH_ANY,
|
||||||
|
FILTER_ASN_ISNULL,
|
||||||
|
FILTER_ASN_GT,
|
||||||
|
FILTER_ASN_LT,
|
||||||
|
FILTER_DOES_NOT_HAVE_CORRESPONDENT,
|
||||||
|
FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE,
|
||||||
|
FILTER_DOES_NOT_HAVE_STORAGE_PATH,
|
||||||
|
FILTER_DOCUMENT_TYPE,
|
||||||
|
FILTER_CORRESPONDENT,
|
||||||
|
FILTER_STORAGE_PATH,
|
||||||
|
} from 'src/app/data/filter-rule-type'
|
||||||
|
import {
|
||||||
|
FilterableDropdownSelectionModel,
|
||||||
|
Intersection,
|
||||||
|
LogicalOperator,
|
||||||
|
} from '../../common/filterable-dropdown/filterable-dropdown.component'
|
||||||
|
import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component'
|
||||||
|
import {
|
||||||
|
DocumentService,
|
||||||
|
SelectionData,
|
||||||
|
SelectionDataItem,
|
||||||
|
} from 'src/app/services/rest/document.service'
|
||||||
|
import { PaperlessDocument } from 'src/app/data/paperless-document'
|
||||||
|
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
||||||
|
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||||
|
import { RelativeDate } from '../../common/date-dropdown/date-dropdown.component'
|
||||||
|
|
||||||
|
const TEXT_FILTER_TARGET_TITLE = 'title'
|
||||||
|
const TEXT_FILTER_TARGET_TITLE_CONTENT = 'title-content'
|
||||||
|
const TEXT_FILTER_TARGET_ASN = 'asn'
|
||||||
|
const TEXT_FILTER_TARGET_FULLTEXT_QUERY = 'fulltext-query'
|
||||||
|
const TEXT_FILTER_TARGET_FULLTEXT_MORELIKE = 'fulltext-morelike'
|
||||||
|
|
||||||
|
const TEXT_FILTER_MODIFIER_EQUALS = 'equals'
|
||||||
|
const TEXT_FILTER_MODIFIER_NULL = 'is null'
|
||||||
|
const TEXT_FILTER_MODIFIER_NOTNULL = 'not null'
|
||||||
|
const TEXT_FILTER_MODIFIER_GT = 'greater'
|
||||||
|
const TEXT_FILTER_MODIFIER_LT = 'less'
|
||||||
|
|
||||||
|
const RELATIVE_DATE_QUERY_REGEXP_CREATED = /created:\[([^\]]+)\]/g
|
||||||
|
const RELATIVE_DATE_QUERY_REGEXP_ADDED = /added:\[([^\]]+)\]/g
|
||||||
|
const RELATIVE_DATE_QUERYSTRINGS = [
|
||||||
|
{
|
||||||
|
relativeDate: RelativeDate.LAST_7_DAYS,
|
||||||
|
dateQuery: '-1 week to now',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
relativeDate: RelativeDate.LAST_MONTH,
|
||||||
|
dateQuery: '-1 month to now',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
relativeDate: RelativeDate.LAST_3_MONTHS,
|
||||||
|
dateQuery: '-3 month to now',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
relativeDate: RelativeDate.LAST_YEAR,
|
||||||
|
dateQuery: '-1 year to now',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-explorer-filter-editor',
|
||||||
|
templateUrl: './filter-editor.component.html',
|
||||||
|
styleUrls: ['./filter-editor.component.scss'],
|
||||||
|
})
|
||||||
|
export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||||
|
generateFilterName() {
|
||||||
|
if (this.filterRules.length == 1) {
|
||||||
|
let rule = this.filterRules[0]
|
||||||
|
switch (this.filterRules[0].rule_type) {
|
||||||
|
case FILTER_HAS_CORRESPONDENT_ANY:
|
||||||
|
if (rule.value) {
|
||||||
|
return $localize`Correspondent: ${
|
||||||
|
this.correspondents.find((c) => c.id == +rule.value)?.name
|
||||||
|
}`
|
||||||
|
} else {
|
||||||
|
return $localize`Without correspondent`
|
||||||
|
}
|
||||||
|
|
||||||
|
case FILTER_HAS_DOCUMENT_TYPE_ANY:
|
||||||
|
if (rule.value) {
|
||||||
|
return $localize`Type: ${
|
||||||
|
this.documentTypes.find((dt) => dt.id == +rule.value)?.name
|
||||||
|
}`
|
||||||
|
} else {
|
||||||
|
return $localize`Without document type`
|
||||||
|
}
|
||||||
|
|
||||||
|
case FILTER_HAS_TAGS_ALL:
|
||||||
|
return $localize`Tag: ${
|
||||||
|
this.tags.find((t) => t.id == +rule.value)?.name
|
||||||
|
}`
|
||||||
|
|
||||||
|
case FILTER_HAS_ANY_TAG:
|
||||||
|
if (rule.value == 'false') {
|
||||||
|
return $localize`Without any tag`
|
||||||
|
}
|
||||||
|
|
||||||
|
case FILTER_TITLE:
|
||||||
|
return $localize`Title: ${rule.value}`
|
||||||
|
|
||||||
|
case FILTER_ASN:
|
||||||
|
return $localize`ASN: ${rule.value}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private documentTypeService: DocumentTypeService,
|
||||||
|
private tagService: TagService,
|
||||||
|
private correspondentService: CorrespondentService,
|
||||||
|
private documentService: DocumentService,
|
||||||
|
private storagePathService: StoragePathService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@ViewChild('textFilterInput')
|
||||||
|
textFilterInput: ElementRef
|
||||||
|
|
||||||
|
tags: PaperlessTag[] = []
|
||||||
|
correspondents: PaperlessCorrespondent[] = []
|
||||||
|
documentTypes: PaperlessDocumentType[] = []
|
||||||
|
storagePaths: PaperlessStoragePath[] = []
|
||||||
|
|
||||||
|
tagDocumentCounts: SelectionDataItem[]
|
||||||
|
correspondentDocumentCounts: SelectionDataItem[]
|
||||||
|
documentTypeDocumentCounts: SelectionDataItem[]
|
||||||
|
storagePathDocumentCounts: SelectionDataItem[]
|
||||||
|
|
||||||
|
_textFilter = ''
|
||||||
|
_moreLikeId: number
|
||||||
|
_moreLikeDoc: PaperlessDocument
|
||||||
|
|
||||||
|
get textFilterTargets() {
|
||||||
|
let targets = [
|
||||||
|
{ id: TEXT_FILTER_TARGET_TITLE, name: $localize`Title` },
|
||||||
|
{
|
||||||
|
id: TEXT_FILTER_TARGET_TITLE_CONTENT,
|
||||||
|
name: $localize`Title & content`,
|
||||||
|
},
|
||||||
|
{ id: TEXT_FILTER_TARGET_ASN, name: $localize`ASN` },
|
||||||
|
{
|
||||||
|
id: TEXT_FILTER_TARGET_FULLTEXT_QUERY,
|
||||||
|
name: $localize`Advanced search`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
if (this.textFilterTarget == TEXT_FILTER_TARGET_FULLTEXT_MORELIKE) {
|
||||||
|
targets.push({
|
||||||
|
id: TEXT_FILTER_TARGET_FULLTEXT_MORELIKE,
|
||||||
|
name: $localize`More like`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return targets
|
||||||
|
}
|
||||||
|
|
||||||
|
textFilterTarget = TEXT_FILTER_TARGET_TITLE_CONTENT
|
||||||
|
|
||||||
|
get textFilterTargetName() {
|
||||||
|
return this.textFilterTargets.find((t) => t.id == this.textFilterTarget)
|
||||||
|
?.name
|
||||||
|
}
|
||||||
|
|
||||||
|
public textFilterModifier: string
|
||||||
|
|
||||||
|
get textFilterModifiers() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: TEXT_FILTER_MODIFIER_EQUALS,
|
||||||
|
label: $localize`equals`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: TEXT_FILTER_MODIFIER_NULL,
|
||||||
|
label: $localize`is empty`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: TEXT_FILTER_MODIFIER_NOTNULL,
|
||||||
|
label: $localize`is not empty`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: TEXT_FILTER_MODIFIER_GT,
|
||||||
|
label: $localize`greater than`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: TEXT_FILTER_MODIFIER_LT,
|
||||||
|
label: $localize`less than`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
get textFilterModifierIsNull(): boolean {
|
||||||
|
return [TEXT_FILTER_MODIFIER_NULL, TEXT_FILTER_MODIFIER_NOTNULL].includes(
|
||||||
|
this.textFilterModifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
tagSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
|
correspondentSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
|
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
|
storagePathSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
|
|
||||||
|
dateCreatedBefore: string
|
||||||
|
dateCreatedAfter: string
|
||||||
|
dateAddedBefore: string
|
||||||
|
dateAddedAfter: string
|
||||||
|
dateCreatedRelativeDate: RelativeDate
|
||||||
|
dateAddedRelativeDate: RelativeDate
|
||||||
|
|
||||||
|
_unmodifiedFilterRules: FilterRule[] = []
|
||||||
|
_filterRules: FilterRule[] = []
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set unmodifiedFilterRules(value: FilterRule[]) {
|
||||||
|
this._unmodifiedFilterRules = value
|
||||||
|
this.rulesModified = filterRulesDiffer(
|
||||||
|
this._unmodifiedFilterRules,
|
||||||
|
this._filterRules
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
get unmodifiedFilterRules(): FilterRule[] {
|
||||||
|
return this._unmodifiedFilterRules
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set filterRules(value: FilterRule[]) {
|
||||||
|
this._filterRules = value
|
||||||
|
|
||||||
|
this.documentTypeSelectionModel.clear(false)
|
||||||
|
this.storagePathSelectionModel.clear(false)
|
||||||
|
this.tagSelectionModel.clear(false)
|
||||||
|
this.correspondentSelectionModel.clear(false)
|
||||||
|
this._textFilter = null
|
||||||
|
this._moreLikeId = null
|
||||||
|
this.dateAddedBefore = null
|
||||||
|
this.dateAddedAfter = null
|
||||||
|
this.dateCreatedBefore = null
|
||||||
|
this.dateCreatedAfter = null
|
||||||
|
this.dateCreatedRelativeDate = null
|
||||||
|
this.dateAddedRelativeDate = null
|
||||||
|
this.textFilterModifier = TEXT_FILTER_MODIFIER_EQUALS
|
||||||
|
|
||||||
|
value.forEach((rule) => {
|
||||||
|
switch (rule.rule_type) {
|
||||||
|
case FILTER_TITLE:
|
||||||
|
this._textFilter = rule.value
|
||||||
|
this.textFilterTarget = TEXT_FILTER_TARGET_TITLE
|
||||||
|
break
|
||||||
|
case FILTER_TITLE_CONTENT:
|
||||||
|
this._textFilter = rule.value
|
||||||
|
this.textFilterTarget = TEXT_FILTER_TARGET_TITLE_CONTENT
|
||||||
|
break
|
||||||
|
case FILTER_ASN:
|
||||||
|
this._textFilter = rule.value
|
||||||
|
this.textFilterTarget = TEXT_FILTER_TARGET_ASN
|
||||||
|
break
|
||||||
|
case FILTER_FULLTEXT_QUERY:
|
||||||
|
let allQueryArgs = rule.value.split(',')
|
||||||
|
let textQueryArgs = []
|
||||||
|
allQueryArgs.forEach((arg) => {
|
||||||
|
if (arg.match(RELATIVE_DATE_QUERY_REGEXP_CREATED)) {
|
||||||
|
;[...arg.matchAll(RELATIVE_DATE_QUERY_REGEXP_CREATED)].forEach(
|
||||||
|
(match) => {
|
||||||
|
if (match[1]?.length) {
|
||||||
|
this.dateCreatedRelativeDate =
|
||||||
|
RELATIVE_DATE_QUERYSTRINGS.find(
|
||||||
|
(qS) => qS.dateQuery == match[1]
|
||||||
|
)?.relativeDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else if (arg.match(RELATIVE_DATE_QUERY_REGEXP_ADDED)) {
|
||||||
|
;[...arg.matchAll(RELATIVE_DATE_QUERY_REGEXP_ADDED)].forEach(
|
||||||
|
(match) => {
|
||||||
|
if (match[1]?.length) {
|
||||||
|
this.dateAddedRelativeDate =
|
||||||
|
RELATIVE_DATE_QUERYSTRINGS.find(
|
||||||
|
(qS) => qS.dateQuery == match[1]
|
||||||
|
)?.relativeDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
textQueryArgs.push(arg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (textQueryArgs.length) {
|
||||||
|
this._textFilter = textQueryArgs.join(',')
|
||||||
|
this.textFilterTarget = TEXT_FILTER_TARGET_FULLTEXT_QUERY
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case FILTER_FULLTEXT_MORELIKE:
|
||||||
|
this._moreLikeId = +rule.value
|
||||||
|
this.textFilterTarget = TEXT_FILTER_TARGET_FULLTEXT_MORELIKE
|
||||||
|
this.documentService.get(this._moreLikeId).subscribe((result) => {
|
||||||
|
this._moreLikeDoc = result
|
||||||
|
this._textFilter = result.title
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case FILTER_CREATED_AFTER:
|
||||||
|
this.dateCreatedAfter = rule.value
|
||||||
|
break
|
||||||
|
case FILTER_CREATED_BEFORE:
|
||||||
|
this.dateCreatedBefore = rule.value
|
||||||
|
break
|
||||||
|
case FILTER_ADDED_AFTER:
|
||||||
|
this.dateAddedAfter = rule.value
|
||||||
|
break
|
||||||
|
case FILTER_ADDED_BEFORE:
|
||||||
|
this.dateAddedBefore = rule.value
|
||||||
|
break
|
||||||
|
case FILTER_HAS_TAGS_ALL:
|
||||||
|
this.tagSelectionModel.logicalOperator = LogicalOperator.And
|
||||||
|
this.tagSelectionModel.set(
|
||||||
|
rule.value ? +rule.value : null,
|
||||||
|
ToggleableItemState.Selected,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case FILTER_HAS_TAGS_ANY:
|
||||||
|
this.tagSelectionModel.logicalOperator = LogicalOperator.Or
|
||||||
|
this.tagSelectionModel.set(
|
||||||
|
rule.value ? +rule.value : null,
|
||||||
|
ToggleableItemState.Selected,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case FILTER_HAS_ANY_TAG:
|
||||||
|
this.tagSelectionModel.set(null, ToggleableItemState.Selected, false)
|
||||||
|
break
|
||||||
|
case FILTER_DOES_NOT_HAVE_TAG:
|
||||||
|
this.tagSelectionModel.set(
|
||||||
|
rule.value ? +rule.value : null,
|
||||||
|
ToggleableItemState.Excluded,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case FILTER_CORRESPONDENT:
|
||||||
|
case FILTER_HAS_CORRESPONDENT_ANY:
|
||||||
|
this.correspondentSelectionModel.logicalOperator = LogicalOperator.Or
|
||||||
|
this.correspondentSelectionModel.intersection = Intersection.Include
|
||||||
|
this.correspondentSelectionModel.set(
|
||||||
|
rule.value ? +rule.value : null,
|
||||||
|
ToggleableItemState.Selected,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case FILTER_DOES_NOT_HAVE_CORRESPONDENT:
|
||||||
|
this.correspondentSelectionModel.intersection = Intersection.Exclude
|
||||||
|
this.correspondentSelectionModel.set(
|
||||||
|
rule.value ? +rule.value : null,
|
||||||
|
ToggleableItemState.Excluded,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case FILTER_DOCUMENT_TYPE:
|
||||||
|
case FILTER_HAS_DOCUMENT_TYPE_ANY:
|
||||||
|
this.documentTypeSelectionModel.logicalOperator = LogicalOperator.Or
|
||||||
|
this.documentTypeSelectionModel.intersection = Intersection.Include
|
||||||
|
this.documentTypeSelectionModel.set(
|
||||||
|
rule.value ? +rule.value : null,
|
||||||
|
ToggleableItemState.Selected,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE:
|
||||||
|
this.documentTypeSelectionModel.intersection = Intersection.Exclude
|
||||||
|
this.documentTypeSelectionModel.set(
|
||||||
|
rule.value ? +rule.value : null,
|
||||||
|
ToggleableItemState.Excluded,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case FILTER_STORAGE_PATH:
|
||||||
|
case FILTER_HAS_STORAGE_PATH_ANY:
|
||||||
|
this.storagePathSelectionModel.logicalOperator = LogicalOperator.Or
|
||||||
|
this.storagePathSelectionModel.intersection = Intersection.Include
|
||||||
|
this.storagePathSelectionModel.set(
|
||||||
|
rule.value ? +rule.value : null,
|
||||||
|
ToggleableItemState.Selected,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case FILTER_DOES_NOT_HAVE_STORAGE_PATH:
|
||||||
|
this.storagePathSelectionModel.intersection = Intersection.Exclude
|
||||||
|
this.storagePathSelectionModel.set(
|
||||||
|
rule.value ? +rule.value : null,
|
||||||
|
ToggleableItemState.Excluded,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case FILTER_ASN_ISNULL:
|
||||||
|
this.textFilterTarget = TEXT_FILTER_TARGET_ASN
|
||||||
|
this.textFilterModifier =
|
||||||
|
rule.value == 'true' || rule.value == '1'
|
||||||
|
? TEXT_FILTER_MODIFIER_NULL
|
||||||
|
: TEXT_FILTER_MODIFIER_NOTNULL
|
||||||
|
break
|
||||||
|
case FILTER_ASN_GT:
|
||||||
|
this.textFilterTarget = TEXT_FILTER_TARGET_ASN
|
||||||
|
this.textFilterModifier = TEXT_FILTER_MODIFIER_GT
|
||||||
|
this._textFilter = rule.value
|
||||||
|
break
|
||||||
|
case FILTER_ASN_LT:
|
||||||
|
this.textFilterTarget = TEXT_FILTER_TARGET_ASN
|
||||||
|
this.textFilterModifier = TEXT_FILTER_MODIFIER_LT
|
||||||
|
this._textFilter = rule.value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.rulesModified = filterRulesDiffer(
|
||||||
|
this._unmodifiedFilterRules,
|
||||||
|
this._filterRules
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
get filterRules(): FilterRule[] {
|
||||||
|
let filterRules: FilterRule[] = []
|
||||||
|
if (
|
||||||
|
this._textFilter &&
|
||||||
|
this.textFilterTarget == TEXT_FILTER_TARGET_TITLE_CONTENT
|
||||||
|
) {
|
||||||
|
filterRules.push({
|
||||||
|
rule_type: FILTER_TITLE_CONTENT,
|
||||||
|
value: this._textFilter,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this._textFilter && this.textFilterTarget == TEXT_FILTER_TARGET_TITLE) {
|
||||||
|
filterRules.push({ rule_type: FILTER_TITLE, value: this._textFilter })
|
||||||
|
}
|
||||||
|
if (this.textFilterTarget == TEXT_FILTER_TARGET_ASN) {
|
||||||
|
if (
|
||||||
|
this.textFilterModifier == TEXT_FILTER_MODIFIER_EQUALS &&
|
||||||
|
this._textFilter
|
||||||
|
) {
|
||||||
|
filterRules.push({ rule_type: FILTER_ASN, value: this._textFilter })
|
||||||
|
} else if (this.textFilterModifierIsNull) {
|
||||||
|
filterRules.push({
|
||||||
|
rule_type: FILTER_ASN_ISNULL,
|
||||||
|
value: (
|
||||||
|
this.textFilterModifier == TEXT_FILTER_MODIFIER_NULL
|
||||||
|
).toString(),
|
||||||
|
})
|
||||||
|
} else if (
|
||||||
|
[TEXT_FILTER_MODIFIER_GT, TEXT_FILTER_MODIFIER_LT].includes(
|
||||||
|
this.textFilterModifier
|
||||||
|
) &&
|
||||||
|
this._textFilter
|
||||||
|
) {
|
||||||
|
filterRules.push({
|
||||||
|
rule_type:
|
||||||
|
this.textFilterModifier == TEXT_FILTER_MODIFIER_GT
|
||||||
|
? FILTER_ASN_GT
|
||||||
|
: FILTER_ASN_LT,
|
||||||
|
value: this._textFilter,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this._textFilter &&
|
||||||
|
this.textFilterTarget == TEXT_FILTER_TARGET_FULLTEXT_QUERY
|
||||||
|
) {
|
||||||
|
filterRules.push({
|
||||||
|
rule_type: FILTER_FULLTEXT_QUERY,
|
||||||
|
value: this._textFilter,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this._moreLikeId &&
|
||||||
|
this.textFilterTarget == TEXT_FILTER_TARGET_FULLTEXT_MORELIKE
|
||||||
|
) {
|
||||||
|
filterRules.push({
|
||||||
|
rule_type: FILTER_FULLTEXT_MORELIKE,
|
||||||
|
value: this._moreLikeId?.toString(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.tagSelectionModel.isNoneSelected()) {
|
||||||
|
filterRules.push({ rule_type: FILTER_HAS_ANY_TAG, value: 'false' })
|
||||||
|
} else {
|
||||||
|
const tagFilterType =
|
||||||
|
this.tagSelectionModel.logicalOperator == LogicalOperator.And
|
||||||
|
? FILTER_HAS_TAGS_ALL
|
||||||
|
: FILTER_HAS_TAGS_ANY
|
||||||
|
this.tagSelectionModel
|
||||||
|
.getSelectedItems()
|
||||||
|
.filter((tag) => tag.id)
|
||||||
|
.forEach((tag) => {
|
||||||
|
filterRules.push({
|
||||||
|
rule_type: tagFilterType,
|
||||||
|
value: tag.id?.toString(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.tagSelectionModel
|
||||||
|
.getExcludedItems()
|
||||||
|
.filter((tag) => tag.id)
|
||||||
|
.forEach((tag) => {
|
||||||
|
filterRules.push({
|
||||||
|
rule_type: FILTER_DOES_NOT_HAVE_TAG,
|
||||||
|
value: tag.id?.toString(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.correspondentSelectionModel.isNoneSelected()) {
|
||||||
|
filterRules.push({ rule_type: FILTER_CORRESPONDENT, value: null })
|
||||||
|
} else {
|
||||||
|
this.correspondentSelectionModel
|
||||||
|
.getSelectedItems()
|
||||||
|
.forEach((correspondent) => {
|
||||||
|
filterRules.push({
|
||||||
|
rule_type: FILTER_HAS_CORRESPONDENT_ANY,
|
||||||
|
value: correspondent.id?.toString(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.correspondentSelectionModel
|
||||||
|
.getExcludedItems()
|
||||||
|
.forEach((correspondent) => {
|
||||||
|
filterRules.push({
|
||||||
|
rule_type: FILTER_DOES_NOT_HAVE_CORRESPONDENT,
|
||||||
|
value: correspondent.id?.toString(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.documentTypeSelectionModel.isNoneSelected()) {
|
||||||
|
filterRules.push({ rule_type: FILTER_DOCUMENT_TYPE, value: null })
|
||||||
|
} else {
|
||||||
|
this.documentTypeSelectionModel
|
||||||
|
.getSelectedItems()
|
||||||
|
.forEach((documentType) => {
|
||||||
|
filterRules.push({
|
||||||
|
rule_type: FILTER_HAS_DOCUMENT_TYPE_ANY,
|
||||||
|
value: documentType.id?.toString(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.documentTypeSelectionModel
|
||||||
|
.getExcludedItems()
|
||||||
|
.forEach((documentType) => {
|
||||||
|
filterRules.push({
|
||||||
|
rule_type: FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE,
|
||||||
|
value: documentType.id?.toString(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.storagePathSelectionModel.isNoneSelected()) {
|
||||||
|
filterRules.push({ rule_type: FILTER_STORAGE_PATH, value: null })
|
||||||
|
} else {
|
||||||
|
this.storagePathSelectionModel
|
||||||
|
.getSelectedItems()
|
||||||
|
.forEach((storagePath) => {
|
||||||
|
filterRules.push({
|
||||||
|
rule_type: FILTER_HAS_STORAGE_PATH_ANY,
|
||||||
|
value: storagePath.id?.toString(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.storagePathSelectionModel
|
||||||
|
.getExcludedItems()
|
||||||
|
.forEach((storagePath) => {
|
||||||
|
filterRules.push({
|
||||||
|
rule_type: FILTER_DOES_NOT_HAVE_STORAGE_PATH,
|
||||||
|
value: storagePath.id?.toString(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.dateCreatedBefore) {
|
||||||
|
filterRules.push({
|
||||||
|
rule_type: FILTER_CREATED_BEFORE,
|
||||||
|
value: this.dateCreatedBefore,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.dateCreatedAfter) {
|
||||||
|
filterRules.push({
|
||||||
|
rule_type: FILTER_CREATED_AFTER,
|
||||||
|
value: this.dateCreatedAfter,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.dateAddedBefore) {
|
||||||
|
filterRules.push({
|
||||||
|
rule_type: FILTER_ADDED_BEFORE,
|
||||||
|
value: this.dateAddedBefore,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.dateAddedAfter) {
|
||||||
|
filterRules.push({
|
||||||
|
rule_type: FILTER_ADDED_AFTER,
|
||||||
|
value: this.dateAddedAfter,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.dateAddedRelativeDate !== null ||
|
||||||
|
this.dateCreatedRelativeDate !== null
|
||||||
|
) {
|
||||||
|
let queryArgs: Array<string> = []
|
||||||
|
let existingRule = filterRules.find(
|
||||||
|
(fr) => fr.rule_type == FILTER_FULLTEXT_QUERY
|
||||||
|
)
|
||||||
|
|
||||||
|
// if had a title / content search and added a relative date we need to carry it over...
|
||||||
|
if (
|
||||||
|
!existingRule &&
|
||||||
|
this._textFilter?.length > 0 &&
|
||||||
|
(this.textFilterTarget == TEXT_FILTER_TARGET_TITLE_CONTENT ||
|
||||||
|
this.textFilterTarget == TEXT_FILTER_TARGET_TITLE)
|
||||||
|
) {
|
||||||
|
existingRule = filterRules.find(
|
||||||
|
(fr) =>
|
||||||
|
fr.rule_type == FILTER_TITLE_CONTENT || fr.rule_type == FILTER_TITLE
|
||||||
|
)
|
||||||
|
existingRule.rule_type = FILTER_FULLTEXT_QUERY
|
||||||
|
}
|
||||||
|
|
||||||
|
let existingRuleArgs = existingRule?.value.split(',')
|
||||||
|
if (this.dateCreatedRelativeDate !== null) {
|
||||||
|
queryArgs.push(
|
||||||
|
`created:[${
|
||||||
|
RELATIVE_DATE_QUERYSTRINGS.find(
|
||||||
|
(qS) => qS.relativeDate == this.dateCreatedRelativeDate
|
||||||
|
).dateQuery
|
||||||
|
}]`
|
||||||
|
)
|
||||||
|
if (existingRule) {
|
||||||
|
queryArgs = existingRuleArgs
|
||||||
|
.filter((arg) => !arg.match(RELATIVE_DATE_QUERY_REGEXP_CREATED))
|
||||||
|
.concat(queryArgs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.dateAddedRelativeDate !== null) {
|
||||||
|
queryArgs.push(
|
||||||
|
`added:[${
|
||||||
|
RELATIVE_DATE_QUERYSTRINGS.find(
|
||||||
|
(qS) => qS.relativeDate == this.dateAddedRelativeDate
|
||||||
|
).dateQuery
|
||||||
|
}]`
|
||||||
|
)
|
||||||
|
if (existingRule) {
|
||||||
|
queryArgs = existingRuleArgs
|
||||||
|
.filter((arg) => !arg.match(RELATIVE_DATE_QUERY_REGEXP_ADDED))
|
||||||
|
.concat(queryArgs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingRule) {
|
||||||
|
existingRule.value = queryArgs.join(',')
|
||||||
|
} else {
|
||||||
|
filterRules.push({
|
||||||
|
rule_type: FILTER_FULLTEXT_QUERY,
|
||||||
|
value: queryArgs.join(','),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.dateCreatedRelativeDate == null &&
|
||||||
|
this.dateAddedRelativeDate == null
|
||||||
|
) {
|
||||||
|
const existingRule = filterRules.find(
|
||||||
|
(fr) => fr.rule_type == FILTER_FULLTEXT_QUERY
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
existingRule?.value.match(RELATIVE_DATE_QUERY_REGEXP_CREATED) ||
|
||||||
|
existingRule?.value.match(RELATIVE_DATE_QUERY_REGEXP_ADDED)
|
||||||
|
) {
|
||||||
|
// remove any existing date query
|
||||||
|
existingRule.value = existingRule.value
|
||||||
|
.replace(RELATIVE_DATE_QUERY_REGEXP_CREATED, '')
|
||||||
|
.replace(RELATIVE_DATE_QUERY_REGEXP_ADDED, '')
|
||||||
|
if (existingRule.value.replace(',', '').trim() === '') {
|
||||||
|
// if its empty now, remove it entirely
|
||||||
|
filterRules.splice(filterRules.indexOf(existingRule), 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filterRules
|
||||||
|
}
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
filterRulesChange = new EventEmitter<FilterRule[]>()
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set selectionData(selectionData: SelectionData) {
|
||||||
|
this.tagDocumentCounts = selectionData?.selected_tags ?? null
|
||||||
|
this.documentTypeDocumentCounts =
|
||||||
|
selectionData?.selected_document_types ?? null
|
||||||
|
this.correspondentDocumentCounts =
|
||||||
|
selectionData?.selected_correspondents ?? null
|
||||||
|
this.storagePathDocumentCounts =
|
||||||
|
selectionData?.selected_storage_paths ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
rulesModified: boolean = false
|
||||||
|
|
||||||
|
updateRules() {
|
||||||
|
this.filterRulesChange.next(this.filterRules)
|
||||||
|
}
|
||||||
|
|
||||||
|
get textFilter() {
|
||||||
|
return this.textFilterModifierIsNull ? '' : this._textFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
set textFilter(value) {
|
||||||
|
this.textFilterDebounce.next(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
textFilterDebounce: Subject<string>
|
||||||
|
subscription: Subscription
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.tagService
|
||||||
|
.listAll()
|
||||||
|
.subscribe((result) => (this.tags = result.results))
|
||||||
|
this.correspondentService
|
||||||
|
.listAll()
|
||||||
|
.subscribe((result) => (this.correspondents = result.results))
|
||||||
|
this.documentTypeService
|
||||||
|
.listAll()
|
||||||
|
.subscribe((result) => (this.documentTypes = result.results))
|
||||||
|
this.storagePathService
|
||||||
|
.listAll()
|
||||||
|
.subscribe((result) => (this.storagePaths = result.results))
|
||||||
|
|
||||||
|
this.textFilterDebounce = new Subject<string>()
|
||||||
|
|
||||||
|
this.subscription = this.textFilterDebounce
|
||||||
|
.pipe(
|
||||||
|
debounceTime(400),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
filter((query) => !query.length || query.length > 2)
|
||||||
|
)
|
||||||
|
.subscribe((text) => this.updateTextFilter(text))
|
||||||
|
|
||||||
|
if (this._textFilter) this.documentService.searchQuery = this._textFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.textFilterDebounce.complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
resetSelected() {
|
||||||
|
this.textFilterTarget = TEXT_FILTER_TARGET_TITLE_CONTENT
|
||||||
|
this.filterRules = this._unmodifiedFilterRules
|
||||||
|
this.updateRules()
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleTag(tagId: number) {
|
||||||
|
this.tagSelectionModel.toggle(tagId)
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleCorrespondent(correspondentId: number) {
|
||||||
|
this.correspondentSelectionModel.toggle(correspondentId)
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleDocumentType(documentTypeId: number) {
|
||||||
|
this.documentTypeSelectionModel.toggle(documentTypeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleStoragePath(storagePathID: number) {
|
||||||
|
this.storagePathSelectionModel.toggle(storagePathID)
|
||||||
|
}
|
||||||
|
|
||||||
|
onTagsDropdownOpen() {
|
||||||
|
this.tagSelectionModel.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
onCorrespondentDropdownOpen() {
|
||||||
|
this.correspondentSelectionModel.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
onDocumentTypeDropdownOpen() {
|
||||||
|
this.documentTypeSelectionModel.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
onStoragePathDropdownOpen() {
|
||||||
|
this.storagePathSelectionModel.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTextFilter(text) {
|
||||||
|
this._textFilter = text
|
||||||
|
this.documentService.searchQuery = text
|
||||||
|
this.updateRules()
|
||||||
|
}
|
||||||
|
|
||||||
|
textFilterKeyup(event: KeyboardEvent) {
|
||||||
|
if (event.key == 'Enter') {
|
||||||
|
const filterString = (
|
||||||
|
this.textFilterInput.nativeElement as HTMLInputElement
|
||||||
|
).value
|
||||||
|
if (filterString.length) {
|
||||||
|
this.updateTextFilter(filterString)
|
||||||
|
}
|
||||||
|
} else if (event.key == 'Escape') {
|
||||||
|
this.resetTextField()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetTextField() {
|
||||||
|
this.updateTextFilter('')
|
||||||
|
}
|
||||||
|
|
||||||
|
changeTextFilterTarget(target) {
|
||||||
|
if (
|
||||||
|
this.textFilterTarget == TEXT_FILTER_TARGET_FULLTEXT_MORELIKE &&
|
||||||
|
target != TEXT_FILTER_TARGET_FULLTEXT_MORELIKE
|
||||||
|
) {
|
||||||
|
this._textFilter = ''
|
||||||
|
}
|
||||||
|
this.textFilterTarget = target
|
||||||
|
this.textFilterInput.nativeElement.focus()
|
||||||
|
this.updateRules()
|
||||||
|
}
|
||||||
|
|
||||||
|
textFilterModifierChange() {
|
||||||
|
if (
|
||||||
|
this.textFilterModifierIsNull ||
|
||||||
|
([
|
||||||
|
TEXT_FILTER_MODIFIER_EQUALS,
|
||||||
|
TEXT_FILTER_MODIFIER_GT,
|
||||||
|
TEXT_FILTER_MODIFIER_LT,
|
||||||
|
].includes(this.textFilterModifier) &&
|
||||||
|
this._textFilter)
|
||||||
|
) {
|
||||||
|
this.updateRules()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user