Get listing storage paths working
This commit is contained in:
parent
62d6c30ee1
commit
c1fc77f55f
@ -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 { APP_INITIALIZER, NgModule } from '@angular/core'
|
||||||
import { AppRoutingModule } from './app-routing.module'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { AppComponent } from './app.component'
|
import { BrowserModule } from '@angular/platform-browser'
|
||||||
import {
|
import {
|
||||||
NgbDateAdapter,
|
NgbDateAdapter,
|
||||||
NgbDateParserFormatter,
|
NgbDateParserFormatter,
|
||||||
NgbModule,
|
NgbModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'
|
import { NgSelectModule } from '@ng-select/ng-select'
|
||||||
import { DocumentListComponent } from './components/document-list/document-list.component'
|
import { PdfViewerModule } from 'ng2-pdf-viewer'
|
||||||
import { ExplorerComponent } from './components/explorer/explorer.component'
|
import { ColorSliderModule } from 'ngx-color/slider'
|
||||||
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
|
import { CookieService } from 'ngx-cookie-service'
|
||||||
import { DashboardComponent } from './components/dashboard/dashboard.component'
|
import { NgxFileDropModule } from 'ngx-file-drop'
|
||||||
import { TagListComponent } from './components/manage/tag-list/tag-list.component'
|
import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'
|
import { AppRoutingModule } from './app-routing.module'
|
||||||
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'
|
import { AppComponent } from './app.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 { 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 { ClearableBadgeComponent } from './components/common/clearable-badge/clearable-badge.component'
|
||||||
import { FilterEditorComponent } from './components/document-list/filter-editor/filter-editor.component'
|
import { ConfirmDialogComponent } from './components/common/confirm-dialog/confirm-dialog.component'
|
||||||
import { FilterEditorComponent as ExplorerFilterEditorComponent } from './components/explorer/filter-editor/filter-editor.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 { 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 { 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 { 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 { 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 { TagsComponent } from './components/common/input/tags/tags.component'
|
||||||
import { IfPermissionsDirective } from './directives/if-permissions.directive'
|
import { TextComponent } from './components/common/input/text/text.component'
|
||||||
import { SortableDirective } from './directives/sortable.directive'
|
import { PageHeaderComponent } from './components/common/page-header/page-header.component'
|
||||||
import { CookieService } from 'ngx-cookie-service'
|
import { PermissionsDialogComponent } from './components/common/permissions-dialog/permissions-dialog.component'
|
||||||
import { CsrfInterceptor } from './interceptors/csrf.interceptor'
|
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 { SavedViewWidgetComponent } from './components/dashboard/widgets/saved-view-widget/saved-view-widget.component'
|
||||||
import { StatisticsWidgetComponent } from './components/dashboard/widgets/statistics-widget/statistics-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 { 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 { WelcomeWidgetComponent } from './components/dashboard/widgets/welcome-widget/welcome-widget.component'
|
||||||
import { YesNoPipe } from './pipes/yes-no.pipe'
|
import { WidgetFrameComponent } from './components/dashboard/widgets/widget-frame/widget-frame.component'
|
||||||
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 { DocumentAsnComponent } from './components/document-asn/document-asn.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 { 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 { DirtyDocGuard } from './guards/dirty-doc.guard'
|
||||||
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
|
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
|
||||||
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
|
import { PermissionsGuard } from './guards/permissions.guard'
|
||||||
import { StoragePathEditDialogComponent } from './components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
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 { SettingsService } from './services/settings.service'
|
||||||
import { TasksComponent } from './components/manage/tasks/tasks.component'
|
import { LocalizedDateParserFormatter } from './utils/ngb-date-parser-formatter'
|
||||||
import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
|
import { ISODateAdapter } from './utils/ngb-iso-date-adapter'
|
||||||
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 localeAr from '@angular/common/locales/ar'
|
import localeAr from '@angular/common/locales/ar'
|
||||||
import localeBe from '@angular/common/locales/be'
|
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 localeSv from '@angular/common/locales/sv'
|
||||||
import localeTr from '@angular/common/locales/tr'
|
import localeTr from '@angular/common/locales/tr'
|
||||||
import localeZh from '@angular/common/locales/zh'
|
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(localeAr)
|
||||||
registerLocaleData(localeBe)
|
registerLocaleData(localeBe)
|
||||||
@ -173,6 +174,7 @@ function initializeApp(settings: SettingsService) {
|
|||||||
DateDropdownComponent,
|
DateDropdownComponent,
|
||||||
DocumentCardLargeComponent,
|
DocumentCardLargeComponent,
|
||||||
DocumentCardSmallComponent,
|
DocumentCardSmallComponent,
|
||||||
|
FolderCardSmallComponent,
|
||||||
BulkEditorComponent,
|
BulkEditorComponent,
|
||||||
TextComponent,
|
TextComponent,
|
||||||
SelectComponent,
|
SelectComponent,
|
||||||
|
@ -1,32 +1,67 @@
|
|||||||
<app-page-header [title]="getTitle()">
|
<app-page-header [title]="getTitle()">
|
||||||
|
|
||||||
<div ngbDropdown class="me-2 d-flex">
|
<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">
|
<svg class="toolbaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#text-indent-left" />
|
<use xlink:href="assets/bootstrap-icons.svg#text-indent-left" />
|
||||||
</svg>
|
</svg>
|
||||||
<div class="d-none d-sm-inline"> <ng-container i18n>Select</ng-container></div>
|
<div class="d-none d-sm-inline">
|
||||||
|
<ng-container i18n>Select</ng-container>
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
|
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
|
||||||
<button ngbDropdownItem (click)="list.selectNone()" i18n>Select none</button>
|
<button ngbDropdownItem (click)="list.selectNone()" i18n>
|
||||||
<button ngbDropdownItem (click)="list.selectPage()" i18n>Select page</button>
|
Select none
|
||||||
<button ngbDropdownItem (click)="list.selectAll()" i18n>Select all</button>
|
</button>
|
||||||
|
<button ngbDropdownItem (click)="list.selectPage()" i18n>
|
||||||
|
Select page
|
||||||
|
</button>
|
||||||
|
<button ngbDropdownItem (click)="list.selectAll()" i18n>
|
||||||
|
Select all
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group flex-fill" role="group">
|
<div class="btn-group flex-fill" role="group">
|
||||||
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="details" (ngModelChange)="saveDisplayMode()" id="displayModeDetails" name="displayModeDetails">
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
[(ngModel)]="displayMode"
|
||||||
|
value="details"
|
||||||
|
(ngModelChange)="saveDisplayMode()"
|
||||||
|
id="displayModeDetails"
|
||||||
|
name="displayModeDetails"
|
||||||
|
/>
|
||||||
<label for="displayModeDetails" class="btn btn-outline-primary btn-sm">
|
<label for="displayModeDetails" class="btn btn-outline-primary btn-sm">
|
||||||
<svg class="toolbaricon" fill="currentColor">
|
<svg class="toolbaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#list-ul" />
|
<use xlink:href="assets/bootstrap-icons.svg#list-ul" />
|
||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="smallCards" (ngModelChange)="saveDisplayMode()" id="displayModeSmall" name="displayModeSmall">
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
[(ngModel)]="displayMode"
|
||||||
|
value="smallCards"
|
||||||
|
(ngModelChange)="saveDisplayMode()"
|
||||||
|
id="displayModeSmall"
|
||||||
|
name="displayModeSmall"
|
||||||
|
/>
|
||||||
<label for="displayModeSmall" class="btn btn-outline-primary btn-sm">
|
<label for="displayModeSmall" class="btn btn-outline-primary btn-sm">
|
||||||
<svg class="toolbaricon" fill="currentColor">
|
<svg class="toolbaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#grid" />
|
<use xlink:href="assets/bootstrap-icons.svg#grid" />
|
||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
<input type="radio" class="btn-check" [(ngModel)]="displayMode" value="largeCards" (ngModelChange)="saveDisplayMode()" id="displayModeLarge" name="displayModeLarge">
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
[(ngModel)]="displayMode"
|
||||||
|
value="largeCards"
|
||||||
|
(ngModelChange)="saveDisplayMode()"
|
||||||
|
id="displayModeLarge"
|
||||||
|
name="displayModeLarge"
|
||||||
|
/>
|
||||||
<label for="displayModeLarge" class="btn btn-outline-primary btn-sm">
|
<label for="displayModeLarge" class="btn btn-outline-primary btn-sm">
|
||||||
<svg class="toolbaricon" fill="currentColor">
|
<svg class="toolbaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#hdd-stack" />
|
<use xlink:href="assets/bootstrap-icons.svg#hdd-stack" />
|
||||||
@ -35,53 +70,115 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ngbDropdown class="btn-group ms-2 flex-fill">
|
<div ngbDropdown class="btn-group ms-2 flex-fill">
|
||||||
<button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle i18n>Sort</button>
|
<button
|
||||||
<div ngbDropdownMenu aria-labelledby="dropdownBasic1" class="shadow dropdown-menu-right">
|
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">
|
<div class="w-100 d-flex pb-2 mb-1 border-bottom">
|
||||||
<input type="radio" class="btn-check" [value]="false" [(ngModel)]="listSortReverse" id="listSortReverseFalse">
|
<input
|
||||||
<label class="btn btn-outline-primary btn-sm mx-2 flex-fill" for="listSortReverseFalse">
|
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">
|
<svg class="toolbaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#sort-alpha-down" />
|
<use xlink:href="assets/bootstrap-icons.svg#sort-alpha-down" />
|
||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
<input type="radio" class="btn-check" [value]="true" [(ngModel)]="listSortReverse" id="listSortReverseTrue">
|
<input
|
||||||
<label class="btn btn-outline-primary btn-sm me-2 flex-fill" for="listSortReverseTrue">
|
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">
|
<svg class="toolbaricon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#sort-alpha-up-alt" />
|
<use xlink:href="assets/bootstrap-icons.svg#sort-alpha-up-alt" />
|
||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button *ngFor="let f of getSortFields()" ngbDropdownItem (click)="setSortField(f.field)"
|
<button
|
||||||
[class.active]="list.sortField === f.field">{{f.name}}
|
*ngFor="let f of getSortFields()"
|
||||||
|
ngbDropdownItem
|
||||||
|
(click)="setSortField(f.field)"
|
||||||
|
[class.active]="list.sortField === f.field"
|
||||||
|
>
|
||||||
|
{{ f.name }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-group ms-2 flex-fill" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }" ngbDropdown role="group">
|
<div
|
||||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle flex-fill" tourAnchor="tour.documents-views" ngbDropdownToggle>
|
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>
|
<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>
|
<span class="visually-hidden">selected</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-menu shadow dropdown-menu-right" ngbDropdownMenu>
|
<div class="dropdown-menu shadow dropdown-menu-right" ngbDropdownMenu>
|
||||||
<ng-container *ngIf="!list.activeSavedViewId">
|
<ng-container *ngIf="!list.activeSavedViewId">
|
||||||
<button ngbDropdownItem *ngFor="let view of savedViewService.allViews" (click)="loadViewConfig(view.id)">{{view.name}}</button>
|
<button
|
||||||
<div class="dropdown-divider" *ngIf="savedViewService.allViews.length > 0"></div>
|
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>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</app-page-header>
|
</app-page-header>
|
||||||
|
|
||||||
<div class="row sticky-top pt-3 pt-sm-4 pb-2 pb-lg-4 bg-body">
|
<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>
|
<app-bulk-editor [hidden]="!isBulkEditing"></app-bulk-editor>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<ng-template #pagination>
|
<ng-template #pagination>
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<p>
|
<p>
|
||||||
@ -89,13 +186,21 @@
|
|||||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
<ng-container i18n>Loading...</ng-container>
|
<ng-container i18n>Loading...</ng-container>
|
||||||
</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>
|
<span i18n *ngIf="list.selected.size > 0"
|
||||||
<ng-container *ngIf="!list.isReloading">
|
>{list.collectionSize, plural, =1 {Selected {{ list.selected.size }} of
|
||||||
<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>
|
one document} other {Selected {{ list.selected.size }} of
|
||||||
</ng-container>
|
{{ list.collectionSize || 0 }} documents}}</span
|
||||||
|
>
|
||||||
</p>
|
</p>
|
||||||
<ngb-pagination *ngIf="list.collectionSize" [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
|
<ngb-pagination
|
||||||
[rotate]="true" aria-label="Default 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>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
@ -103,131 +208,246 @@
|
|||||||
<ng-container *ngTemplateOutlet="pagination"></ng-container>
|
<ng-container *ngTemplateOutlet="pagination"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="list.error ; else documentListNoError">
|
<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-container>
|
||||||
|
|
||||||
<ng-template #documentListNoError>
|
<ng-template #documentListNoError>
|
||||||
<div *ngIf="displayMode === 'largeCards'">
|
<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>
|
</app-document-card-large>
|
||||||
</div>
|
</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>
|
<thead>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th class="d-none d-lg-table-cell"
|
<th
|
||||||
|
class="d-none d-lg-table-cell"
|
||||||
appSortable="archive_serial_number"
|
appSortable="archive_serial_number"
|
||||||
title="Sort by ASN" i18n-title
|
title="Sort by ASN"
|
||||||
|
i18n-title
|
||||||
[currentSortField]="list.sortField"
|
[currentSortField]="list.sortField"
|
||||||
[currentSortReverse]="list.sortReverse"
|
[currentSortReverse]="list.sortReverse"
|
||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
i18n>ASN</th>
|
i18n
|
||||||
<th class="d-none d-md-table-cell"
|
>
|
||||||
|
ASN
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="d-none d-md-table-cell"
|
||||||
appSortable="correspondent__name"
|
appSortable="correspondent__name"
|
||||||
title="Sort by correspondent" i18n-title
|
title="Sort by correspondent"
|
||||||
|
i18n-title
|
||||||
[currentSortField]="list.sortField"
|
[currentSortField]="list.sortField"
|
||||||
[currentSortReverse]="list.sortReverse"
|
[currentSortReverse]="list.sortReverse"
|
||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
i18n>Correspondent</th>
|
i18n
|
||||||
|
>
|
||||||
|
Correspondent
|
||||||
|
</th>
|
||||||
<th
|
<th
|
||||||
appSortable="title"
|
appSortable="title"
|
||||||
title="Sort by title" i18n-title
|
title="Sort by title"
|
||||||
|
i18n-title
|
||||||
[currentSortField]="list.sortField"
|
[currentSortField]="list.sortField"
|
||||||
[currentSortReverse]="list.sortReverse"
|
[currentSortReverse]="list.sortReverse"
|
||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
i18n>Title</th>
|
i18n
|
||||||
<th *ngIf="notesEnabled" class="d-none d-xl-table-cell"
|
>
|
||||||
|
Title
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
*ngIf="notesEnabled"
|
||||||
|
class="d-none d-xl-table-cell"
|
||||||
appSortable="num_notes"
|
appSortable="num_notes"
|
||||||
title="Sort by notes" i18n-title
|
title="Sort by notes"
|
||||||
|
i18n-title
|
||||||
[currentSortField]="list.sortField"
|
[currentSortField]="list.sortField"
|
||||||
[currentSortReverse]="list.sortReverse"
|
[currentSortReverse]="list.sortReverse"
|
||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
i18n>Notes</th>
|
i18n
|
||||||
<th class="d-none d-xl-table-cell"
|
>
|
||||||
|
Notes
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="d-none d-xl-table-cell"
|
||||||
appSortable="document_type__name"
|
appSortable="document_type__name"
|
||||||
title="Sort by document type" i18n-title
|
title="Sort by document type"
|
||||||
|
i18n-title
|
||||||
[currentSortField]="list.sortField"
|
[currentSortField]="list.sortField"
|
||||||
[currentSortReverse]="list.sortReverse"
|
[currentSortReverse]="list.sortReverse"
|
||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
i18n>Document type</th>
|
i18n
|
||||||
<th class="d-none d-xl-table-cell"
|
>
|
||||||
|
Document type
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="d-none d-xl-table-cell"
|
||||||
appSortable="storage_path__name"
|
appSortable="storage_path__name"
|
||||||
title="Sort by storage path" i18n-title
|
title="Sort by storage path"
|
||||||
|
i18n-title
|
||||||
[currentSortField]="list.sortField"
|
[currentSortField]="list.sortField"
|
||||||
[currentSortReverse]="list.sortReverse"
|
[currentSortReverse]="list.sortReverse"
|
||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
i18n>Storage path</th>
|
i18n
|
||||||
|
>
|
||||||
|
Storage path
|
||||||
|
</th>
|
||||||
<th
|
<th
|
||||||
appSortable="created"
|
appSortable="created"
|
||||||
title="Sort by created date" i18n-title
|
title="Sort by created date"
|
||||||
|
i18n-title
|
||||||
[currentSortField]="list.sortField"
|
[currentSortField]="list.sortField"
|
||||||
[currentSortReverse]="list.sortReverse"
|
[currentSortReverse]="list.sortReverse"
|
||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
i18n>Created</th>
|
i18n
|
||||||
<th class="d-none d-xl-table-cell"
|
>
|
||||||
|
Created
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="d-none d-xl-table-cell"
|
||||||
appSortable="added"
|
appSortable="added"
|
||||||
title="Sort by added date" i18n-title
|
title="Sort by added date"
|
||||||
|
i18n-title
|
||||||
[currentSortField]="list.sortField"
|
[currentSortField]="list.sortField"
|
||||||
[currentSortReverse]="list.sortReverse"
|
[currentSortReverse]="list.sortReverse"
|
||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
i18n>Added</th>
|
i18n
|
||||||
|
>
|
||||||
|
Added
|
||||||
|
</th>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<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>
|
<td>
|
||||||
<div class="form-check">
|
<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
|
||||||
<label class="form-check-label" for="docCheck{{d.id}}"></label>
|
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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="d-none d-lg-table-cell">
|
<td class="d-none d-lg-table-cell">
|
||||||
{{d.archive_serial_number}}
|
{{ d.archive_serial_number }}
|
||||||
</td>
|
</td>
|
||||||
<td class="d-none d-md-table-cell">
|
<td class="d-none d-md-table-cell">
|
||||||
<ng-container *ngIf="d.correspondent">
|
<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>
|
</ng-container>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
|
<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>
|
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>
|
||||||
<td *ngIf="notesEnabled" class="d-none d-xl-table-cell">
|
<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">
|
<span class="badge rounded-pill bg-light border text-primary">
|
||||||
<svg class="metadata-icon ms-1 me-1" fill="currentColor">
|
<svg class="metadata-icon ms-1 me-1" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text"/>
|
<use xlink:href="assets/bootstrap-icons.svg#chat-left-text" />
|
||||||
</svg>
|
</svg>
|
||||||
{{d.notes.length}}</span>
|
{{ d.notes.length }}</span
|
||||||
|
>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="d-none d-xl-table-cell">
|
<td class="d-none d-xl-table-cell">
|
||||||
<ng-container *ngIf="d.document_type">
|
<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>
|
</ng-container>
|
||||||
</td>
|
</td>
|
||||||
<td class="d-none d-xl-table-cell">
|
<td class="d-none d-xl-table-cell">
|
||||||
<ng-container *ngIf="d.storage_path">
|
<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>
|
</ng-container>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{d.created_date | customDate}}
|
{{ d.created_date | customDate }}
|
||||||
</td>
|
</td>
|
||||||
<td class="d-none d-xl-table-cell">
|
<td class="d-none d-xl-table-cell">
|
||||||
{{d.added | customDate}}
|
{{ d.added | customDate }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class="row row-cols-paperless-cards" *ngIf="displayMode === 'smallCards'">
|
<div
|
||||||
<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>
|
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>
|
||||||
<div *ngIf="list.documents?.length > 15" class="mt-3">
|
<div *ngIf="list.documents?.length > 15" class="mt-3">
|
||||||
<ng-container *ngTemplateOutlet="pagination"></ng-container>
|
<ng-container *ngTemplateOutlet="pagination"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -6,9 +6,9 @@ import {
|
|||||||
ViewChild,
|
ViewChild,
|
||||||
ViewChildren,
|
ViewChildren,
|
||||||
} from '@angular/core'
|
} 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 { 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 {
|
import {
|
||||||
FilterRule,
|
FilterRule,
|
||||||
filterRulesDiffer,
|
filterRulesDiffer,
|
||||||
@ -19,11 +19,10 @@ import { PaperlessDocument } from 'src/app/data/paperless-document'
|
|||||||
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
|
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||||
import {
|
import {
|
||||||
SortableDirective,
|
|
||||||
SortEvent,
|
SortEvent,
|
||||||
|
SortableDirective,
|
||||||
} from 'src/app/directives/sortable.directive'
|
} from 'src/app/directives/sortable.directive'
|
||||||
import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
|
import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
|
||||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
|
||||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||||
import {
|
import {
|
||||||
DOCUMENT_SORT_FIELDS,
|
DOCUMENT_SORT_FIELDS,
|
||||||
@ -31,6 +30,7 @@ import {
|
|||||||
} from 'src/app/services/rest/document.service'
|
} from 'src/app/services/rest/document.service'
|
||||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
import { StoragePathListViewService } from 'src/app/services/storage-path-list-view.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
||||||
import { FilterEditorComponent } from './filter-editor/filter-editor.component'
|
import { FilterEditorComponent } from './filter-editor/filter-editor.component'
|
||||||
@ -45,7 +45,7 @@ export class ExplorerComponent
|
|||||||
implements OnInit, OnDestroy
|
implements OnInit, OnDestroy
|
||||||
{
|
{
|
||||||
constructor(
|
constructor(
|
||||||
public list: DocumentListViewService,
|
public list: StoragePathListViewService,
|
||||||
public savedViewService: SavedViewService,
|
public savedViewService: SavedViewService,
|
||||||
public route: ActivatedRoute,
|
public route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
@ -170,13 +170,14 @@ export class ExplorerComponent
|
|||||||
takeUntil(this.unsubscribeNotifier)
|
takeUntil(this.unsubscribeNotifier)
|
||||||
)
|
)
|
||||||
.subscribe((queryParams) => {
|
.subscribe((queryParams) => {
|
||||||
|
console.log('test')
|
||||||
if (queryParams.has('view')) {
|
if (queryParams.has('view')) {
|
||||||
// loading a saved view on /documents
|
// loading a saved view on /documents
|
||||||
this.loadViewConfig(parseInt(queryParams.get('view')))
|
this.loadViewConfig(parseInt(queryParams.get('view')))
|
||||||
} else {
|
} else {
|
||||||
// this.list.activateSavedView(null)
|
this.list.activateSavedView(null)
|
||||||
// this.list.loadFromQueryParams(queryParams)
|
this.list.loadFromQueryParams(queryParams)
|
||||||
// this.unmodifiedFilterRules = []
|
this.unmodifiedFilterRules = []
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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>
|
@ -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;
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
143
src-ui/src/app/services/rest/custom-storage-path.service.ts
Normal file
143
src-ui/src/app/services/rest/custom-storage-path.service.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
517
src-ui/src/app/services/storage-path-list-view.service.ts
Normal file
517
src-ui/src/app/services/storage-path-list-view.service.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user