Get listing storage paths working

This commit is contained in:
Martin Tan 2023-05-28 19:58:12 +08:00
parent 62d6c30ee1
commit c1fc77f55f
9 changed files with 1312 additions and 158 deletions

View File

@ -1,95 +1,98 @@
import { BrowserModule } from '@angular/platform-browser'
import { DatePipe, registerLocaleData } from '@angular/common'
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'
import { APP_INITIALIZER, NgModule } from '@angular/core'
import { AppRoutingModule } from './app-routing.module'
import { AppComponent } from './app.component'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { BrowserModule } from '@angular/platform-browser'
import {
NgbDateAdapter,
NgbDateParserFormatter,
NgbModule,
} from '@ng-bootstrap/ng-bootstrap'
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'
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 { DashboardComponent } from './components/dashboard/dashboard.component'
import { TagListComponent } from './components/manage/tag-list/tag-list.component'
import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'
import { LogsComponent } from './components/manage/logs/logs.component'
import { SettingsComponent } from './components/manage/settings/settings.component'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { DatePipe, registerLocaleData } from '@angular/common'
import { NotFoundComponent } from './components/not-found/not-found.component'
import { ConfirmDialogComponent } from './components/common/confirm-dialog/confirm-dialog.component'
import { CorrespondentEditDialogComponent } from './components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
import { TagEditDialogComponent } from './components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
import { DocumentTypeEditDialogComponent } from './components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
import { TagComponent } from './components/common/tag/tag.component'
import { ClearableBadgeComponent } from './components/common/clearable-badge/clearable-badge.component'
import { PageHeaderComponent } from './components/common/page-header/page-header.component'
import { NgSelectModule } from '@ng-select/ng-select'
import { PdfViewerModule } from 'ng2-pdf-viewer'
import { ColorSliderModule } from 'ngx-color/slider'
import { CookieService } from 'ngx-cookie-service'
import { NgxFileDropModule } from 'ngx-file-drop'
import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
import { AppRoutingModule } from './app-routing.module'
import { AppComponent } from './app.component'
import { AppFrameComponent } from './components/app-frame/app-frame.component'
import { ToastsComponent } from './components/common/toasts/toasts.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 { ClearableBadgeComponent } from './components/common/clearable-badge/clearable-badge.component'
import { ConfirmDialogComponent } from './components/common/confirm-dialog/confirm-dialog.component'
import { DateDropdownComponent } from './components/common/date-dropdown/date-dropdown.component'
import { CorrespondentEditDialogComponent } from './components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
import { DocumentTypeEditDialogComponent } from './components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
import { GroupEditDialogComponent } from './components/common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
import { MailAccountEditDialogComponent } from './components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component'
import { MailRuleEditDialogComponent } from './components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component'
import { StoragePathEditDialogComponent } from './components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
import { TagEditDialogComponent } from './components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
import { UserEditDialogComponent } from './components/common/edit-dialog/user-edit-dialog/user-edit-dialog.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 { DateDropdownComponent } from './components/common/date-dropdown/date-dropdown.component'
import { DocumentCardLargeComponent } from './components/document-list/document-card-large/document-card-large.component'
import { DocumentCardSmallComponent } from './components/document-list/document-card-small/document-card-small.component'
import { BulkEditorComponent } from './components/document-list/bulk-editor/bulk-editor.component'
import { NgxFileDropModule } from 'ngx-file-drop'
import { TextComponent } from './components/common/input/text/text.component'
import { SelectComponent } from './components/common/input/select/select.component'
import { CheckComponent } from './components/common/input/check/check.component'
import { ColorComponent } from './components/common/input/color/color.component'
import { DateComponent } from './components/common/input/date/date.component'
import { NumberComponent } from './components/common/input/number/number.component'
import { PasswordComponent } from './components/common/input/password/password.component'
import { SaveViewConfigDialogComponent } from './components/document-list/save-view-config-dialog/save-view-config-dialog.component'
import { PermissionsFormComponent } from './components/common/input/permissions/permissions-form/permissions-form.component'
import { PermissionsGroupComponent } from './components/common/input/permissions/permissions-group/permissions-group.component'
import { PermissionsUserComponent } from './components/common/input/permissions/permissions-user/permissions-user.component'
import { SelectComponent } from './components/common/input/select/select.component'
import { TagsComponent } from './components/common/input/tags/tags.component'
import { IfPermissionsDirective } from './directives/if-permissions.directive'
import { SortableDirective } from './directives/sortable.directive'
import { CookieService } from 'ngx-cookie-service'
import { CsrfInterceptor } from './interceptors/csrf.interceptor'
import { TextComponent } from './components/common/input/text/text.component'
import { PageHeaderComponent } from './components/common/page-header/page-header.component'
import { PermissionsDialogComponent } from './components/common/permissions-dialog/permissions-dialog.component'
import { PermissionsSelectComponent } from './components/common/permissions-select/permissions-select.component'
import { SelectDialogComponent } from './components/common/select-dialog/select-dialog.component'
import { TagComponent } from './components/common/tag/tag.component'
import { ToastsComponent } from './components/common/toasts/toasts.component'
import { DashboardComponent } from './components/dashboard/dashboard.component'
import { SavedViewWidgetComponent } from './components/dashboard/widgets/saved-view-widget/saved-view-widget.component'
import { StatisticsWidgetComponent } from './components/dashboard/widgets/statistics-widget/statistics-widget.component'
import { UploadFileWidgetComponent } from './components/dashboard/widgets/upload-file-widget/upload-file-widget.component'
import { WidgetFrameComponent } from './components/dashboard/widgets/widget-frame/widget-frame.component'
import { PdfViewerModule } from 'ng2-pdf-viewer'
import { WelcomeWidgetComponent } from './components/dashboard/widgets/welcome-widget/welcome-widget.component'
import { YesNoPipe } from './pipes/yes-no.pipe'
import { FileSizePipe } from './pipes/file-size.pipe'
import { FilterPipe } from './pipes/filter.pipe'
import { DocumentTitlePipe } from './pipes/document-title.pipe'
import { MetadataCollapseComponent } from './components/document-detail/metadata-collapse/metadata-collapse.component'
import { SelectDialogComponent } from './components/common/select-dialog/select-dialog.component'
import { NgSelectModule } from '@ng-select/ng-select'
import { NumberComponent } from './components/common/input/number/number.component'
import { SafeUrlPipe } from './pipes/safeurl.pipe'
import { SafeHtmlPipe } from './pipes/safehtml.pipe'
import { CustomDatePipe } from './pipes/custom-date.pipe'
import { DateComponent } from './components/common/input/date/date.component'
import { ISODateAdapter } from './utils/ngb-iso-date-adapter'
import { LocalizedDateParserFormatter } from './utils/ngb-date-parser-formatter'
import { ApiVersionInterceptor } from './interceptors/api-version.interceptor'
import { ColorSliderModule } from 'ngx-color/slider'
import { ColorComponent } from './components/common/input/color/color.component'
import { WidgetFrameComponent } from './components/dashboard/widgets/widget-frame/widget-frame.component'
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
import { MetadataCollapseComponent } from './components/document-detail/metadata-collapse/metadata-collapse.component'
import { BulkEditorComponent } from './components/document-list/bulk-editor/bulk-editor.component'
import { DocumentCardLargeComponent } from './components/document-list/document-card-large/document-card-large.component'
import { DocumentCardSmallComponent } from './components/document-list/document-card-small/document-card-small.component'
import { DocumentListComponent } from './components/document-list/document-list.component'
import { FilterEditorComponent } from './components/document-list/filter-editor/filter-editor.component'
import { SaveViewConfigDialogComponent } from './components/document-list/save-view-config-dialog/save-view-config-dialog.component'
import { DocumentNotesComponent } from './components/document-notes/document-notes.component'
import { PermissionsGuard } from './guards/permissions.guard'
import { ExplorerComponent } from './components/explorer/explorer.component'
import { FilterEditorComponent as ExplorerFilterEditorComponent } from './components/explorer/filter-editor/filter-editor.component'
import { FolderCardSmallComponent } from './components/explorer/folder-card-small/folder-card-small.component'
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'
import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'
import { LogsComponent } from './components/manage/logs/logs.component'
import { SettingsComponent } from './components/manage/settings/settings.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 { NotFoundComponent } from './components/not-found/not-found.component'
import { IfObjectPermissionsDirective } from './directives/if-object-permissions.directive'
import { IfOwnerDirective } from './directives/if-owner.directive'
import { IfPermissionsDirective } from './directives/if-permissions.directive'
import { SortableDirective } from './directives/sortable.directive'
import { DirtyDocGuard } from './guards/dirty-doc.guard'
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
import { StoragePathEditDialogComponent } from './components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
import { PermissionsGuard } from './guards/permissions.guard'
import { ApiVersionInterceptor } from './interceptors/api-version.interceptor'
import { CsrfInterceptor } from './interceptors/csrf.interceptor'
import { CustomDatePipe } from './pipes/custom-date.pipe'
import { DocumentTitlePipe } from './pipes/document-title.pipe'
import { FileSizePipe } from './pipes/file-size.pipe'
import { FilterPipe } from './pipes/filter.pipe'
import { SafeHtmlPipe } from './pipes/safehtml.pipe'
import { SafeUrlPipe } from './pipes/safeurl.pipe'
import { YesNoPipe } from './pipes/yes-no.pipe'
import { SettingsService } from './services/settings.service'
import { TasksComponent } from './components/manage/tasks/tasks.component'
import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
import { UserEditDialogComponent } from './components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
import { GroupEditDialogComponent } from './components/common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
import { PermissionsSelectComponent } from './components/common/permissions-select/permissions-select.component'
import { MailAccountEditDialogComponent } from './components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component'
import { MailRuleEditDialogComponent } from './components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component'
import { PermissionsUserComponent } from './components/common/input/permissions/permissions-user/permissions-user.component'
import { PermissionsGroupComponent } from './components/common/input/permissions/permissions-group/permissions-group.component'
import { IfOwnerDirective } from './directives/if-owner.directive'
import { IfObjectPermissionsDirective } from './directives/if-object-permissions.directive'
import { LocalizedDateParserFormatter } from './utils/ngb-date-parser-formatter'
import { ISODateAdapter } from './utils/ngb-iso-date-adapter'
import localeAr from '@angular/common/locales/ar'
import localeBe from '@angular/common/locales/be'
@ -111,8 +114,6 @@ import localeSr from '@angular/common/locales/sr'
import localeSv from '@angular/common/locales/sv'
import localeTr from '@angular/common/locales/tr'
import localeZh from '@angular/common/locales/zh'
import { PermissionsDialogComponent } from './components/common/permissions-dialog/permissions-dialog.component'
import { PermissionsFormComponent } from './components/common/input/permissions/permissions-form/permissions-form.component'
registerLocaleData(localeAr)
registerLocaleData(localeBe)
@ -173,6 +174,7 @@ function initializeApp(settings: SettingsService) {
DateDropdownComponent,
DocumentCardLargeComponent,
DocumentCardSmallComponent,
FolderCardSmallComponent,
BulkEditorComponent,
TextComponent,
SelectComponent,

View File

@ -1,32 +1,67 @@
<app-page-header [title]="getTitle()">
<div ngbDropdown class="me-2 d-flex">
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
<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">&nbsp;<ng-container i18n>Select</ng-container></div>
<div class="d-none d-sm-inline">
&nbsp;<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>
<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">
<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">
<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">
<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" />
@ -35,53 +70,115 @@
</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">
<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">
<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">
<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
*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>
<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">
<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>
<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-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>
@ -89,13 +186,21 @@
<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>&nbsp;<span i18n *ngIf="isFiltered">(filtered)</span>
</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
>
</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>
<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>
@ -104,80 +209,147 @@
</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>
<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
[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'">
<table
class="table table-sm align-middle border shadow-sm"
*ngIf="displayMode === 'details'"
>
<thead>
<th></th>
<th class="d-none d-lg-table-cell"
<th
class="d-none d-lg-table-cell"
appSortable="archive_serial_number"
title="Sort by ASN" i18n-title
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"
i18n
>
ASN
</th>
<th
class="d-none d-md-table-cell"
appSortable="correspondent__name"
title="Sort by correspondent" i18n-title
title="Sort by correspondent"
i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Correspondent</th>
i18n
>
Correspondent
</th>
<th
appSortable="title"
title="Sort by title" i18n-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"
i18n
>
Title
</th>
<th
*ngIf="notesEnabled"
class="d-none d-xl-table-cell"
appSortable="num_notes"
title="Sort by notes" i18n-title
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"
i18n
>
Notes
</th>
<th
class="d-none d-xl-table-cell"
appSortable="document_type__name"
title="Sort by document type" i18n-title
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"
i18n
>
Document type
</th>
<th
class="d-none d-xl-table-cell"
appSortable="storage_path__name"
title="Sort by storage path" i18n-title
title="Sort by storage path"
i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Storage path</th>
i18n
>
Storage path
</th>
<th
appSortable="created"
title="Sort by created date" i18n-title
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"
i18n
>
Created
</th>
<th
class="d-none d-xl-table-cell"
appSortable="added"
title="Sort by added date" i18n-title
title="Sort by added date"
i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Added</th>
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' : ''">
<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();">
<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>
@ -186,30 +358,70 @@
</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>
<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>
<a
routerLink="/explorer/{{ 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">
<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>
{{ 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>
<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>
<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>
@ -222,12 +434,20 @@
</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
class="row row-cols-paperless-cards"
*ngIf="displayMode === 'smallCards'"
>
<app-folder-card-small
class="p-0"
[selected]="list.isSelected(d)"
(toggleSelected)="toggleSelected(d, $event)"
(dblClickDocument)="openDocumentDetail(d)"
[storagePath]="d"
*ngFor="let d of list.documents; trackBy: trackByDocumentId"
></app-folder-card-small>
</div>
<div *ngIf="list.documents?.length > 15" class="mt-3">
<ng-container *ngTemplateOutlet="pagination"></ng-container>
</div>
</ng-template>

View File

@ -6,9 +6,9 @@ import {
ViewChild,
ViewChildren,
} from '@angular/core'
import { ActivatedRoute, convertToParamMap, Router } from '@angular/router'
import { ActivatedRoute, Router, convertToParamMap } from '@angular/router'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { filter, first, map, Subject, switchMap, takeUntil } from 'rxjs'
import { Subject, filter, first, map, switchMap, takeUntil } from 'rxjs'
import {
FilterRule,
filterRulesDiffer,
@ -19,11 +19,10 @@ 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,
SortableDirective,
} 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,
@ -31,6 +30,7 @@ import {
} 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 { StoragePathListViewService } from 'src/app/services/storage-path-list-view.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'
@ -45,7 +45,7 @@ export class ExplorerComponent
implements OnInit, OnDestroy
{
constructor(
public list: DocumentListViewService,
public list: StoragePathListViewService,
public savedViewService: SavedViewService,
public route: ActivatedRoute,
private router: Router,
@ -170,13 +170,14 @@ export class ExplorerComponent
takeUntil(this.unsubscribeNotifier)
)
.subscribe((queryParams) => {
console.log('test')
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 = []
this.list.activateSavedView(null)
this.list.loadFromQueryParams(queryParams)
this.unmodifiedFilterRules = []
}
})
}

View File

@ -0,0 +1,50 @@
<div class="col p-2 h-100">
<div
class="card h-100 shadow-sm document-card"
[class.card-selected]="selected"
[class.popover-hidden]="popoverHidden"
(mouseleave)="mouseLeaveCard()"
>
<div
class="border-bottom doc-img-container"
[class.doc-img-background-selected]="selected"
(click)="this.toggleSelected.emit($event)"
(dblclick)="dblClickDocument.emit(this)"
>
<div class="card-img doc-img rounded-top">
<svg
width="100%"
height="100%"
preserveAspectRatio="xMidYMid meet"
fill="currentColor"
>
<use xlink:href="assets/bootstrap-icons.svg#folder" />
</svg>
</div>
<div
class="border-end border-bottom bg-light py-1 px-2 document-card-check"
>
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
id="smallCardCheck{{ storagePath.id }}"
[checked]="selected"
(click)="this.toggleSelected.emit($event)"
/>
<label
class="form-check-label"
for="smallCardCheck{{ storagePath.id }}"
></label>
</div>
</div>
</div>
<div class="card-body bg-light p-2">
<p class="card-text">
{{ storagePath.name }}
</p>
</div>
</div>
</div>

View File

@ -0,0 +1,92 @@
.card-text {
font-size: 90%;
}
.doc-img {
object-fit: cover;
object-position: top left;
height: 90px;
mix-blend-mode: multiply;
display: flex;
justify-content: center;
align-items: center;
padding: 30px;
}
.document-card-check {
display: none;
position: absolute;
top: 0;
left: 0;
padding: 0.5rem;
border-top-left-radius: 0.25rem;
border-bottom-right-radius: 0.25rem;
pointer-events: none;
.form-check {
padding: 0;
min-height: 0;
margin-bottom: 0;
.form-check-input {
margin-left: 0;
}
}
}
.document-card:hover .document-card-check {
display: block;
}
.document-card-notes {
position: absolute;
right: 0;
top: 142px;
}
.card-selected {
border-color:var(--bs-primary);
.document-card-check {
display: block;
}
}
.doc-img-background-selected {
background-color: var(--pngx-primary-faded);
}
.card-info {
line-height: 1;
button {
line-height: 1;
&:hover,
&:focus {
background-color: transparent !important;
color: var(--bs-primary);
}
}
}
.card-footer .btn {
padding-top: .10rem;
}
::ng-deep .tooltip-inner {
text-align: left !important;
font-size: 90%;
}
a {
cursor: pointer;
}
.tags {
top: .2rem;
right: 0;
max-width: 80%;
row-gap: .2rem;
line-height: 1;
}

View File

@ -0,0 +1,107 @@
import {
Component,
EventEmitter,
Input,
Output,
ViewChild,
} from '@angular/core'
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
import { map } from 'rxjs/operators'
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
@Component({
selector: 'app-folder-card-small',
templateUrl: './folder-card-small.component.html',
styleUrls: [
'./folder-card-small.component.scss',
'../popover-preview/popover-preview.scss',
],
})
export class FolderCardSmallComponent extends ComponentWithPermissions {
constructor(
private storagePathService: StoragePathService,
private settingsService: SettingsService
) {
super()
}
@Input()
selected = false
@Output()
toggleSelected = new EventEmitter()
@Input()
storagePath: PaperlessStoragePath
@Output()
dblClickDocument = new EventEmitter()
@Output()
clickTag = new EventEmitter<number>()
@Output()
clickCorrespondent = new EventEmitter<number>()
@Output()
clickDocumentType = new EventEmitter<number>()
@Output()
clickStoragePath = new EventEmitter<number>()
moreTags: number = null
@ViewChild('popover') popover: NgbPopover
mouseOnPreview = false
popoverHidden = true
getIsThumbInverted() {
return this.settingsService.get(SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED)
}
getThumbUrl() {
return ''
}
getDownloadUrl() {
return ''
}
get previewUrl() {
return ''
}
mouseEnterPreview() {
this.mouseOnPreview = true
if (!this.popover.isOpen()) {
// we're going to open but hide to pre-load content during hover delay
this.popover.open()
this.popoverHidden = true
setTimeout(() => {
if (this.mouseOnPreview) {
// show popover
this.popoverHidden = false
} else {
this.popover.close()
}
}, 600)
}
}
mouseLeavePreview() {
this.mouseOnPreview = false
}
mouseLeaveCard() {
this.popover.close()
}
get notesEnabled(): boolean {
return this.settingsService.get(SETTINGS_KEYS.NOTES_ENABLED)
}
}

View File

@ -0,0 +1,22 @@
::ng-deep app-document-list .popover {
max-width: 40rem;
.preview {
min-width: 30rem;
min-height: 18rem;
max-height: 35rem;
overflow-y: scroll;
}
.spinner-border {
position: absolute;
top: 4rem;
left: calc(50% - 0.5rem);
z-index: 0;
}
}
::ng-deep .popover-hidden .popover {
opacity: 0;
pointer-events: none;
}

View File

@ -0,0 +1,143 @@
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { Observable, map } from 'rxjs'
import { FilterRule } from 'src/app/data/filter-rule'
import { PaperlessDocument } from 'src/app/data/paperless-document'
import { PaperlessDocumentMetadata } from 'src/app/data/paperless-document-metadata'
import { PaperlessDocumentSuggestions } from 'src/app/data/paperless-document-suggestions'
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
import { Results } from 'src/app/data/results'
import { queryParamsFromFilterRules } from 'src/app/utils/query-params'
import { AbstractPaperlessService } from './abstract-paperless-service'
interface SelectionDataItem {
id: number
document_count: number
}
interface SelectionData {
selected_storage_paths: SelectionDataItem[]
selected_correspondents: SelectionDataItem[]
selected_tags: SelectionDataItem[]
selected_document_types: SelectionDataItem[]
}
@Injectable({
providedIn: 'root',
})
export class CustomStoragePathService extends AbstractPaperlessService<PaperlessStoragePath> {
private _searchQuery: string
constructor(http: HttpClient) {
super(http, 'storage_paths')
}
listFiltered(
page?: number,
pageSize?: number,
sortField?: string,
sortReverse?: boolean,
filterRules?: FilterRule[],
extraParams = {}
): Observable<Results<PaperlessStoragePath>> {
return this.list(
page,
pageSize,
sortField,
sortReverse,
Object.assign(extraParams, queryParamsFromFilterRules(filterRules))
).pipe(
map((results) => {
return results
})
)
}
listAllFilteredIds(filterRules?: FilterRule[]): Observable<number[]> {
return this.listFiltered(1, 100000, null, null, filterRules, {
fields: 'id',
}).pipe(map((response) => response.results.map((doc) => doc.id)))
}
getPreviewUrl(id: number, original: boolean = false): string {
let url = this.getResourceUrl(id, 'preview')
if (this._searchQuery) url += `#search="${this._searchQuery}"`
if (original) {
url += '?original=true'
}
return url
}
getThumbUrl(id: number): string {
return this.getResourceUrl(id, 'thumb')
}
getDownloadUrl(id: number, original: boolean = false): string {
let url = this.getResourceUrl(id, 'download')
if (original) {
url += '?original=true'
}
return url
}
update(o: PaperlessDocument): Observable<PaperlessDocument> {
// we want to only set created_date
o.created = undefined
return super.update(o)
}
uploadDocument(formData) {
return this.http.post(
this.getResourceUrl(null, 'post_document'),
formData,
{ reportProgress: true, observe: 'events' }
)
}
getMetadata(id: number): Observable<PaperlessDocumentMetadata> {
return this.http.get<PaperlessDocumentMetadata>(
this.getResourceUrl(id, 'metadata')
)
}
bulkEdit(ids: number[], method: string, args: any) {
return this.http.post(this.getResourceUrl(null, 'bulk_edit'), {
documents: ids,
method: method,
parameters: args,
})
}
getSelectionData(ids: number[]): Observable<SelectionData> {
return this.http.post<SelectionData>(
this.getResourceUrl(null, 'selection_data'),
{ documents: ids }
)
}
getSuggestions(id: number): Observable<PaperlessDocumentSuggestions> {
return this.http.get<PaperlessDocumentSuggestions>(
this.getResourceUrl(id, 'suggestions')
)
}
bulkDownload(
ids: number[],
content = 'both',
useFilenameFormatting: boolean = false
) {
return this.http.post(
this.getResourceUrl(null, 'bulk_download'),
{
documents: ids,
content: content,
follow_formatting: useFilenameFormatting,
},
{ responseType: 'blob' }
)
}
public set searchQuery(query: string) {
this._searchQuery = query
}
}

View File

@ -0,0 +1,517 @@
import { Injectable } from '@angular/core'
import { ParamMap, Router } from '@angular/router'
import { Observable } from 'rxjs'
import {
FilterRule,
cloneFilterRules,
filterRulesDiffer,
isFullTextFilterRule,
} from '../data/filter-rule'
import { PaperlessDocument } from '../data/paperless-document'
import { PaperlessSavedView } from '../data/paperless-saved-view'
import { PaperlessStoragePath } from '../data/paperless-storage-path'
import { SETTINGS_KEYS } from '../data/paperless-uisettings'
import { DOCUMENT_LIST_SERVICE } from '../data/storage-keys'
import { paramsFromViewState, paramsToViewState } from '../utils/query-params'
import { CustomStoragePathService } from './rest/custom-storage-path.service'
import { DOCUMENT_SORT_FIELDS, SelectionData } from './rest/document.service'
import { SettingsService } from './settings.service'
/**
* Captures the current state of the list view.
*/
export interface ListViewState {
/**
* Title of the document list view. Either "Documents" (localized) or the name of a saved view.
*/
title?: string
/**
* Current paginated list of storage paths displayed.
*/
storagePaths?: PaperlessStoragePath[]
currentPage: number
/**
* Total amount of documents with the current filter rules. Used to calculate the number of pages.
*/
collectionSize?: number
/**
* Currently selected sort field.
*/
sortField: string
/**
* True if the list is sorted in reverse.
*/
sortReverse: boolean
/**
* Filter rules for the current list view.
*/
filterRules: FilterRule[]
/**
* Contains the IDs of all selected documents.
*/
selected?: Set<number>
}
/**
* This service manages the document list which is displayed using the document list view.
*
* This service also serves saved views by transparently switching between the document list
* and saved views on request. See below.
*/
@Injectable({
providedIn: 'root',
})
export class StoragePathListViewService {
isReloading: boolean = false
initialized: boolean = false
error: string = null
rangeSelectionAnchorIndex: number
lastRangeSelectionToIndex: number
selectionData?: SelectionData
currentPageSize: number = this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)
private listViewStates: Map<number, ListViewState> = new Map()
private _activeSavedViewId: number = null
get activeSavedViewId() {
return this._activeSavedViewId
}
get activeSavedViewTitle() {
return this.activeListViewState.title
}
constructor(
private storagePathService: CustomStoragePathService,
private settings: SettingsService,
private router: Router
) {}
private defaultListViewState(): ListViewState {
return {
title: null,
storagePaths: [],
currentPage: 1,
collectionSize: null,
sortField: 'created',
sortReverse: true,
filterRules: [],
selected: new Set<number>(),
}
}
private get activeListViewState() {
if (!this.listViewStates.has(this._activeSavedViewId)) {
this.listViewStates.set(
this._activeSavedViewId,
this.defaultListViewState()
)
}
return this.listViewStates.get(this._activeSavedViewId)
}
activateSavedView(view: PaperlessSavedView) {
this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null
if (view) {
this._activeSavedViewId = view.id
this.loadSavedView(view)
} else {
this._activeSavedViewId = null
}
}
activateSavedViewWithQueryParams(
view: PaperlessSavedView,
queryParams: ParamMap
) {
const viewState = paramsToViewState(queryParams)
this.activateSavedView(view)
this.currentPage = viewState.currentPage
}
loadSavedView(view: PaperlessSavedView, closeCurrentView: boolean = false) {
if (closeCurrentView) {
this._activeSavedViewId = null
}
this.activeListViewState.filterRules = cloneFilterRules(view.filter_rules)
this.activeListViewState.sortField = view.sort_field
this.activeListViewState.sortReverse = view.sort_reverse
if (this._activeSavedViewId) {
this.activeListViewState.title = view.name
}
this.reduceSelectionToFilter()
if (!this.router.routerState.snapshot.url.includes('/view/')) {
this.router.navigate(['view', view.id])
}
}
loadFromQueryParams(queryParams: ParamMap) {
const paramsEmpty: boolean = queryParams.keys.length == 0
let newState: ListViewState = this.listViewStates.get(
this._activeSavedViewId
)
if (!paramsEmpty) newState = paramsToViewState(queryParams)
if (newState == undefined) newState = this.defaultListViewState() // if nothing in local storage
// only reload if things have changed
if (
!this.initialized ||
paramsEmpty ||
this.activeListViewState.sortField !== newState.sortField ||
this.activeListViewState.sortReverse !== newState.sortReverse ||
this.activeListViewState.currentPage !== newState.currentPage ||
filterRulesDiffer(
this.activeListViewState.filterRules,
newState.filterRules
)
) {
this.activeListViewState.filterRules = newState.filterRules
this.activeListViewState.sortField = newState.sortField
this.activeListViewState.sortReverse = newState.sortReverse
this.activeListViewState.currentPage = newState.currentPage
this.reload(null, paramsEmpty) // update the params if there arent any
}
}
reload(onFinish?, updateQueryParams: boolean = true) {
this.isReloading = true
this.error = null
let activeListViewState = this.activeListViewState
this.storagePathService
.listFiltered(
activeListViewState.currentPage,
this.currentPageSize,
activeListViewState.sortField,
activeListViewState.sortReverse,
activeListViewState.filterRules,
{ truncate_content: true }
)
.subscribe({
next: (result) => {
console.log('list filtered result:', result)
this.initialized = true
this.isReloading = false
activeListViewState.collectionSize = result.count
activeListViewState.storagePaths = result.results
this.storagePathService
.getSelectionData(result.results.map((d) => d.id))
.subscribe({
next: (selectionData) => {
this.selectionData = selectionData
},
error: () => {
this.selectionData = null
},
})
// if (updateQueryParams && !this._activeSavedViewId) {
// let base = ['/documents']
// this.router.navigate(base, {
// queryParams: paramsFromViewState(activeListViewState),
// replaceUrl: !this.router.routerState.snapshot.url.includes('?'), // in case navigating from params-less /documents
// })
// } else if (this._activeSavedViewId) {
// this.router.navigate([], {
// queryParams: paramsFromViewState(activeListViewState, true),
// queryParamsHandling: 'merge',
// })
// }
if (onFinish) {
onFinish()
}
this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null
},
error: (error) => {
this.isReloading = false
if (activeListViewState.currentPage != 1 && error.status == 404) {
// this happens when applying a filter: the current page might not be available anymore due to the reduced result set.
activeListViewState.currentPage = 1
this.reload()
} else {
this.selectionData = null
let errorMessage
if (
typeof error.error !== 'string' &&
Object.keys(error.error).length > 0
) {
// e.g. { archive_serial_number: Array<string> }
errorMessage = Object.keys(error.error)
.map((fieldName) => {
const fieldError: Array<string> = error.error[fieldName]
return `${
DOCUMENT_SORT_FIELDS.find((f) => f.field == fieldName)?.name
}: ${fieldError[0]}`
})
.join(', ')
} else {
errorMessage = error.error
}
this.error = errorMessage
}
},
})
}
set filterRules(filterRules: FilterRule[]) {
if (
!isFullTextFilterRule(filterRules) &&
this.activeListViewState.sortField == 'score'
) {
this.activeListViewState.sortField = 'created'
}
this.activeListViewState.filterRules = filterRules
this.reload()
this.reduceSelectionToFilter()
this.saveDocumentListView()
}
get filterRules(): FilterRule[] {
return this.activeListViewState.filterRules
}
set sortField(field: string) {
this.activeListViewState.sortField = field
this.reload()
this.saveDocumentListView()
}
get sortField(): string {
return this.activeListViewState.sortField
}
set sortReverse(reverse: boolean) {
this.activeListViewState.sortReverse = reverse
this.reload()
this.saveDocumentListView()
}
get sortReverse(): boolean {
return this.activeListViewState.sortReverse
}
get collectionSize(): number {
return this.activeListViewState.collectionSize
}
get currentPage(): number {
return this.activeListViewState.currentPage
}
set currentPage(page: number) {
if (this.activeListViewState.currentPage == page) return
this.activeListViewState.currentPage = page
this.reload()
this.saveDocumentListView()
}
get documents(): PaperlessDocument[] {
return this.activeListViewState.storagePaths
}
get selected(): Set<number> {
return this.activeListViewState.selected
}
setSort(field: string, reverse: boolean) {
this.activeListViewState.sortField = field
this.activeListViewState.sortReverse = reverse
this.reload()
this.saveDocumentListView()
}
private saveDocumentListView() {
if (this._activeSavedViewId == null) {
let savedState: ListViewState = {
collectionSize: this.activeListViewState.collectionSize,
currentPage: this.activeListViewState.currentPage,
filterRules: this.activeListViewState.filterRules,
sortField: this.activeListViewState.sortField,
sortReverse: this.activeListViewState.sortReverse,
}
localStorage.setItem(
DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG,
JSON.stringify(savedState)
)
}
}
quickFilter(filterRules: FilterRule[]) {
this._activeSavedViewId = null
this.filterRules = filterRules
}
getLastPage(): number {
return Math.ceil(this.collectionSize / this.currentPageSize)
}
hasNext(doc: number) {
if (this.documents) {
let index = this.documents.findIndex((d) => d.id == doc)
return (
index != -1 &&
(this.currentPage < this.getLastPage() ||
index + 1 < this.documents.length)
)
}
}
hasPrevious(doc: number) {
if (this.documents) {
let index = this.documents.findIndex((d) => d.id == doc)
return index != -1 && !(index == 0 && this.currentPage == 1)
}
}
getNext(currentDocId: number): Observable<number> {
return new Observable((nextDocId) => {
if (this.documents != null) {
let index = this.documents.findIndex((d) => d.id == currentDocId)
if (index != -1 && index + 1 < this.documents.length) {
nextDocId.next(this.documents[index + 1].id)
nextDocId.complete()
} else if (index != -1 && this.currentPage < this.getLastPage()) {
this.currentPage += 1
this.reload(() => {
nextDocId.next(this.documents[0].id)
nextDocId.complete()
})
} else {
nextDocId.complete()
}
} else {
nextDocId.complete()
}
})
}
getPrevious(currentDocId: number): Observable<number> {
return new Observable((prevDocId) => {
if (this.documents != null) {
let index = this.documents.findIndex((d) => d.id == currentDocId)
if (index != 0) {
prevDocId.next(this.documents[index - 1].id)
prevDocId.complete()
} else if (this.currentPage > 1) {
this.currentPage -= 1
this.reload(() => {
prevDocId.next(this.documents[this.documents.length - 1].id)
prevDocId.complete()
})
} else {
prevDocId.complete()
}
} else {
prevDocId.complete()
}
})
}
updatePageSize() {
let newPageSize = this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)
if (newPageSize != this.currentPageSize) {
this.currentPageSize = newPageSize
}
}
selectNone() {
this.selected.clear()
this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null
}
reduceSelectionToFilter() {
if (this.selected.size > 0) {
this.storagePathService
.listAllFilteredIds(this.filterRules)
.subscribe((ids) => {
for (let id of this.selected) {
if (!ids.includes(id)) {
this.selected.delete(id)
}
}
})
}
}
selectAll() {
this.storagePathService
.listAllFilteredIds(this.filterRules)
.subscribe((ids) => ids.forEach((id) => this.selected.add(id)))
}
selectPage() {
this.selected.clear()
this.documents.forEach((doc) => {
this.selected.add(doc.id)
})
}
isSelected(d: PaperlessDocument) {
return this.selected.has(d.id)
}
toggleSelected(d: PaperlessDocument): void {
if (this.selected.has(d.id)) this.selected.delete(d.id)
else this.selected.add(d.id)
this.rangeSelectionAnchorIndex = this.documentIndexInCurrentView(d.id)
this.lastRangeSelectionToIndex = null
}
selectRangeTo(d: PaperlessDocument) {
if (this.rangeSelectionAnchorIndex !== null) {
const documentToIndex = this.documentIndexInCurrentView(d.id)
const fromIndex = Math.min(
this.rangeSelectionAnchorIndex,
documentToIndex
)
const toIndex = Math.max(this.rangeSelectionAnchorIndex, documentToIndex)
if (this.lastRangeSelectionToIndex !== null) {
// revert the old selection
this.documents
.slice(
Math.min(
this.rangeSelectionAnchorIndex,
this.lastRangeSelectionToIndex
),
Math.max(
this.rangeSelectionAnchorIndex,
this.lastRangeSelectionToIndex
) + 1
)
.forEach((d) => {
this.selected.delete(d.id)
})
}
this.documents.slice(fromIndex, toIndex + 1).forEach((d) => {
this.selected.add(d.id)
})
this.lastRangeSelectionToIndex = documentToIndex
} else {
// e.g. shift key but was first click
this.toggleSelected(d)
}
}
documentIndexInCurrentView(documentID: number): number {
return this.documents.map((d) => d.id).indexOf(documentID)
}
}