mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-28 18:24:38 -05:00
Merge branch 'dev' into l10n_dev
This commit is contained in:
@@ -46,7 +46,7 @@ describe('settings', () => {
|
||||
})
|
||||
})
|
||||
|
||||
cy.viewport(1024, 1024)
|
||||
cy.viewport(1024, 1600)
|
||||
cy.visit('/settings')
|
||||
cy.wait('@savedViews')
|
||||
})
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
873
src-ui/package-lock.json
generated
873
src-ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,48 +13,49 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/common": "~14.2.0",
|
||||
"@angular/compiler": "~14.2.0",
|
||||
"@angular/core": "~14.2.0",
|
||||
"@angular/forms": "~14.2.0",
|
||||
"@angular/localize": "~14.2.0",
|
||||
"@angular/platform-browser": "~14.2.0",
|
||||
"@angular/platform-browser-dynamic": "~14.2.0",
|
||||
"@angular/router": "~14.2.0",
|
||||
"@angular/common": "~14.2.8",
|
||||
"@angular/compiler": "~14.2.8",
|
||||
"@angular/core": "~14.2.8",
|
||||
"@angular/forms": "~14.2.8",
|
||||
"@angular/localize": "~14.2.8",
|
||||
"@angular/platform-browser": "~14.2.8",
|
||||
"@angular/platform-browser-dynamic": "~14.2.8",
|
||||
"@angular/router": "~14.2.8",
|
||||
"@ng-bootstrap/ng-bootstrap": "^13.0.0",
|
||||
"@ng-select/ng-select": "^9.0.2",
|
||||
"@ngneat/dirty-check-forms": "^3.0.2",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"bootstrap": "^5.2.0",
|
||||
"bootstrap": "^5.2.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"ng2-pdf-viewer": "^9.1.0",
|
||||
"ngx-color": "^8.0.2",
|
||||
"ng2-pdf-viewer": "^9.1.2",
|
||||
"ngx-color": "^8.0.3",
|
||||
"ngx-cookie-service": "^14.0.1",
|
||||
"ngx-file-drop": "^14.0.1",
|
||||
"rxjs": "~7.5.6",
|
||||
"ngx-ui-tour-ng-bootstrap": "^11.1.0",
|
||||
"rxjs": "~7.5.7",
|
||||
"tslib": "^2.3.1",
|
||||
"uuid": "^8.3.1",
|
||||
"uuid": "^9.0.0",
|
||||
"zone.js": "~0.11.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/jest": "14.0.1",
|
||||
"@angular-devkit/build-angular": "~14.2.1",
|
||||
"@angular/cli": "~14.2.1",
|
||||
"@angular/compiler-cli": "~14.2.0",
|
||||
"@angular-devkit/build-angular": "~14.2.7",
|
||||
"@angular/cli": "~14.2.7",
|
||||
"@angular/compiler-cli": "~14.2.8",
|
||||
"@types/jest": "28.1.6",
|
||||
"@types/node": "^18.7.14",
|
||||
"@types/node": "^18.7.23",
|
||||
"codelyzer": "^6.0.2",
|
||||
"concurrently": "7.3.0",
|
||||
"concurrently": "7.4.0",
|
||||
"jest": "28.1.3",
|
||||
"jest-environment-jsdom": "^29.0.1",
|
||||
"jest-environment-jsdom": "^29.2.2",
|
||||
"jest-preset-angular": "^12.2.2",
|
||||
"ts-node": "~10.9.1",
|
||||
"tslint": "~6.1.3",
|
||||
"typescript": "~4.7.4",
|
||||
"typescript": "~4.8.4",
|
||||
"wait-on": "~6.0.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@cypress/schematic": "^2.1.1",
|
||||
"cypress": "~10.7.0"
|
||||
"cypress": "~10.9.0"
|
||||
}
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@ import { DirtyFormGuard } from './guards/dirty-form.guard'
|
||||
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
|
||||
import { TasksComponent } from './components/manage/tasks/tasks.component'
|
||||
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
||||
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
|
||||
@@ -24,8 +25,16 @@ const routes: Routes = [
|
||||
canDeactivate: [DirtyDocGuard],
|
||||
children: [
|
||||
{ path: 'dashboard', component: DashboardComponent },
|
||||
{ path: 'documents', component: DocumentListComponent },
|
||||
{ path: 'view/:id', component: DocumentListComponent },
|
||||
{
|
||||
path: 'documents',
|
||||
component: DocumentListComponent,
|
||||
canDeactivate: [DirtySavedViewGuard],
|
||||
},
|
||||
{
|
||||
path: 'view/:id',
|
||||
component: DocumentListComponent,
|
||||
canDeactivate: [DirtySavedViewGuard],
|
||||
},
|
||||
{ path: 'documents/:id', component: DocumentDetailComponent },
|
||||
{ path: 'asn/:id', component: DocumentAsnComponent },
|
||||
{ path: 'tags', component: TagListComponent },
|
||||
|
@@ -11,3 +11,28 @@
|
||||
</div>
|
||||
</ng-template>
|
||||
</ngx-file-drop>
|
||||
|
||||
<tour-step-template>
|
||||
<ng-template #tourStep let-step="step">
|
||||
<p class="tour-step-content" [innerHTML]="step?.content"></p>
|
||||
<hr/>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="badge bg-light text-dark">{{ tourService.steps?.indexOf(step) + 1 }} / {{ tourService.steps?.length }}</span>
|
||||
<div class="tour-step-navigation btn-toolbar" role="toolbar" aria-label="Controls">
|
||||
<div class="btn-group btn-group-sm me-2" role="group" aria-label="Dismiss">
|
||||
<button class="btn btn-outline-danger" (click)="tourService.end()">
|
||||
{{ step?.endBtnTitle }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm align-self-end" role="group" aria-label="Previous / Next">
|
||||
<button *ngIf="tourService.hasPrev(step)" class="btn btn-outline-primary" (click)="tourService.prev()">
|
||||
« {{ step?.prevBtnTitle }}
|
||||
</button>
|
||||
<button *ngIf="tourService.hasNext(step)" class="btn btn-outline-primary" (click)="tourService.next()">
|
||||
{{ step?.nextBtnTitle }} »
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</tour-step-template>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { SettingsService } from './services/settings.service'
|
||||
import { SETTINGS_KEYS } from './data/paperless-uisettings'
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { Component, OnDestroy, OnInit, Renderer2 } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { ConsumerStatusService } from './services/consumer-status.service'
|
||||
@@ -8,6 +8,7 @@ import { ToastService } from './services/toast.service'
|
||||
import { NgxFileDropEntry } from 'ngx-file-drop'
|
||||
import { UploadDocumentsService } from './services/upload-documents.service'
|
||||
import { TasksService } from './services/tasks.service'
|
||||
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -29,7 +30,9 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
private toastService: ToastService,
|
||||
private router: Router,
|
||||
private uploadDocumentsService: UploadDocumentsService,
|
||||
private tasksService: TasksService
|
||||
private tasksService: TasksService,
|
||||
public tourService: TourService,
|
||||
private renderer: Renderer2
|
||||
) {
|
||||
let anyWindow = window as any
|
||||
anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.js'
|
||||
@@ -112,6 +115,87 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
this.tourService.initialize([
|
||||
{
|
||||
anchorId: 'tour.dashboard',
|
||||
content: $localize`The dashboard can be used to show saved views, such as an 'Inbox'. Those settings are found under Settings > Saved Views once you have created some.`,
|
||||
route: '/dashboard',
|
||||
enableBackdrop: true,
|
||||
delayAfterNavigation: 500,
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.upload-widget',
|
||||
content: $localize`Drag-and-drop documents here to start uploading or place them in the consume folder. You can also drag-and-drop documents anywhere on all other pages of the web app. Once you do, Paperless-ngx will start training its machine learning algorithms.`,
|
||||
route: '/dashboard',
|
||||
enableBackdrop: true,
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.documents',
|
||||
content: $localize`The documents list shows all of your documents and allows for filtering as well as bulk-editing. There are three different view styles: list, small cards and large cards. A list of documents currently opened for editing is shown in the sidebar.`,
|
||||
route: '/documents?sort=created&reverse=1&page=1',
|
||||
delayAfterNavigation: 500,
|
||||
placement: 'bottom',
|
||||
enableBackdrop: true,
|
||||
disableScrollToAnchor: true,
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.documents-filter-editor',
|
||||
content: $localize`The filtering tools allow you to quickly find documents using various searches, dates, tags, etc.`,
|
||||
route: '/documents?sort=created&reverse=1&page=1',
|
||||
placement: 'bottom',
|
||||
enableBackdrop: true,
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.documents-views',
|
||||
content: $localize`Any combination of filters can be saved as a 'view' which can then be displayed on the dashboard and / or sidebar.`,
|
||||
route: '/documents?sort=created&reverse=1&page=1',
|
||||
enableBackdrop: true,
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.tags',
|
||||
content: $localize`Tags, correspondents, document types and storage paths can all be managed using these pages. They can also be created from the document edit view.`,
|
||||
route: '/tags',
|
||||
enableBackdrop: true,
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.file-tasks',
|
||||
content: $localize`File Tasks shows you documents that have been consumed, are waiting to be, or may have failed during the process.`,
|
||||
route: '/tasks',
|
||||
enableBackdrop: true,
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.settings',
|
||||
content: $localize`Check out the settings for various tweaks to the web app or to toggle settings for saved views.`,
|
||||
route: '/settings',
|
||||
enableBackdrop: true,
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.admin',
|
||||
content: $localize`The Admin area contains more advanced controls as well as the settings for automatic e-mail fetching.`,
|
||||
enableBackdrop: true,
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.outro',
|
||||
title: $localize`Thank you! 🙏`,
|
||||
content:
|
||||
$localize`There are <em>tons</em> more features and info we didn't cover here, but this should get you started. Check out the documentation or visit the project on GitHub to learn more or to report issues.` +
|
||||
'<br/><br/>' +
|
||||
$localize`Lastly, on behalf of every contributor to this community-supported project, thank you for using Paperless-ngx!`,
|
||||
route: '/dashboard',
|
||||
},
|
||||
])
|
||||
|
||||
this.tourService.start$.subscribe(() => {
|
||||
this.renderer.addClass(document.body, 'tour-active')
|
||||
})
|
||||
|
||||
this.tourService.end$.subscribe(() => {
|
||||
// animation time
|
||||
setTimeout(() => {
|
||||
this.renderer.removeClass(document.body, 'tour-active')
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
public get dragDropEnabled(): boolean {
|
||||
|
@@ -24,6 +24,7 @@ import { CorrespondentEditDialogComponent } from './components/common/edit-dialo
|
||||
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 { ClearableBadge } 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 { ToastsComponent } from './components/common/toasts/toasts.component'
|
||||
@@ -69,6 +70,12 @@ import { ColorComponent } from './components/common/input/color/color.component'
|
||||
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
||||
import { DocumentCommentsComponent } from './components/document-comments/document-comments.component'
|
||||
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 { SettingsService } from './services/settings.service'
|
||||
import { TasksComponent } from './components/manage/tasks/tasks.component'
|
||||
import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
|
||||
|
||||
import localeBe from '@angular/common/locales/be'
|
||||
import localeCs from '@angular/common/locales/cs'
|
||||
@@ -89,10 +96,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 { 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 { SettingsService } from './services/settings.service'
|
||||
import { TasksComponent } from './components/manage/tasks/tasks.component'
|
||||
|
||||
registerLocaleData(localeBe)
|
||||
registerLocaleData(localeCs)
|
||||
@@ -140,6 +143,7 @@ function initializeApp(settings: SettingsService) {
|
||||
DocumentTypeEditDialogComponent,
|
||||
StoragePathEditDialogComponent,
|
||||
TagComponent,
|
||||
ClearableBadge,
|
||||
PageHeaderComponent,
|
||||
AppFrameComponent,
|
||||
ToastsComponent,
|
||||
@@ -188,6 +192,7 @@ function initializeApp(settings: SettingsService) {
|
||||
PdfViewerModule,
|
||||
NgSelectModule,
|
||||
ColorSliderModule,
|
||||
TourNgBootstrapModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
@@ -213,6 +218,7 @@ function initializeApp(settings: SettingsService) {
|
||||
{ provide: NgbDateAdapter, useClass: ISODateAdapter },
|
||||
{ provide: NgbDateParserFormatter, useClass: LocalizedDateParserFormatter },
|
||||
DirtyDocGuard,
|
||||
DirtySavedViewGuard,
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
|
@@ -4,11 +4,11 @@
|
||||
(click)="isMenuCollapsed = !isMenuCollapsed">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<a class="navbar-brand col-auto col-md-3 col-lg-2 me-0 px-3 py-3 order-sm-0" routerLink="/dashboard">
|
||||
<a class="navbar-brand col-auto col-md-3 col-lg-2 me-0 px-3 py-3 order-sm-0" [ngClass]="slimSidebarEnabled ? 'slim' : 'col-auto col-md-3 col-lg-2'" routerLink="/dashboard" tourAnchor="tour.intro">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.43 238.91" width="1em" class="me-2" fill="currentColor">
|
||||
<path d="M194.7,0C164.22,70.94,17.64,79.74,64.55,194.06c.58,1.47-10.85,17-18.47,29.9-1.76-6.45-3.81-13.48-3.52-14.07,38.11-45.14-27.26-70.65-30.78-107.58C-4.64,131.62-10.5,182.92,39,212.53c.3,0,2.64,11.14,3.81,16.71a58.55,58.55,0,0,0-2.93,6.45c-1.17,2.93,7.62,2.64,7.62,3.22.88-.29,21.7-36.93,22.28-37.23C187.67,174.72,208.48,68.6,194.7,0ZM134.61,74.75C79.5,124,70.12,160.64,71.88,178.53,53.41,134.85,107.64,86.77,134.61,74.75ZM28.2,145.11c10.55,9.67,28.14,39.28,13.19,56.57C44.91,193.77,46.08,175.89,28.2,145.11Z" transform="translate(0 0)"/>
|
||||
</svg>
|
||||
<ng-container i18n="app title">Paperless-ngx</ng-container>
|
||||
<span class="ms-2" [class.visually-hidden]="slimSidebarEnabled" i18n="app title">Paperless-ngx</span>
|
||||
</a>
|
||||
<div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 ps-md-4 me-sm-auto order-3 order-sm-1">
|
||||
<form (ngSubmit)="search()" class="form-inline flex-grow-1">
|
||||
@@ -16,7 +16,12 @@
|
||||
<use xlink:href="assets/bootstrap-icons.svg#search"/>
|
||||
</svg>
|
||||
<input class="form-control form-control-sm" type="text" placeholder="Search documents" aria-label="Search"
|
||||
[formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (selectItem)="itemSelected($event)" i18n-placeholder>
|
||||
[formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (keyup)="searchFieldKeyup($event)" (selectItem)="itemSelected($event)" i18n-placeholder>
|
||||
<button *ngIf="!searchFieldEmpty" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0" (click)="resetSearchField()">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x me-1" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<ul ngbNav class="order-sm-3">
|
||||
@@ -51,48 +56,54 @@
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse" [ngbCollapse]="isMenuCollapsed">
|
||||
<nav id="sidebarMenu" class="d-md-block bg-light sidebar collapse" [ngClass]="slimSidebarEnabled ? 'slim' : 'col-md-3 col-lg-2'" [class.animating]="slimSidebarAnimating" [ngbCollapse]="isMenuCollapsed">
|
||||
<button class="btn btn-sm btn-dark sidebar-slim-toggler" (click)="toggleSlimSidebar()">
|
||||
<svg class="sidebaricon-sm" fill="currentColor">
|
||||
<use *ngIf="slimSidebarEnabled" xlink:href="assets/bootstrap-icons.svg#chevron-double-right"/>
|
||||
<use *ngIf="!slimSidebarEnabled" xlink:href="assets/bootstrap-icons.svg#chevron-double-left"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="sidebar-sticky pt-3 d-flex flex-column justify-space-around">
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="dashboard" routerLinkActive="active" (click)="closeMenu()">
|
||||
<a class="nav-link" routerLink="dashboard" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Dashboard" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#house"/>
|
||||
</svg> <ng-container i18n>Dashboard</ng-container>
|
||||
</svg><span> <ng-container i18n>Dashboard</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="documents" routerLinkActive="active" (click)="closeMenu()">
|
||||
<a class="nav-link" routerLink="documents" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Documents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#files"/>
|
||||
</svg> <ng-container i18n>Documents</ng-container>
|
||||
</svg><span> <ng-container i18n>Documents</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted" *ngIf='savedViewService.loading || savedViewService.sidebarViews.length > 0'>
|
||||
<ng-container i18n>Saved views</ng-container>
|
||||
<span i18n>Saved views</span>
|
||||
<div *ngIf="savedViewService.loading" class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div>
|
||||
</h6>
|
||||
<ul class="nav flex-column mb-2">
|
||||
<li class="nav-item w-100" *ngFor="let view of savedViewService.sidebarViews">
|
||||
<a class="nav-link text-truncate" routerLink="view/{{view.id}}" routerLinkActive="active" (click)="closeMenu()">
|
||||
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="view/{{view.id}}" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name" [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#funnel"/>
|
||||
</svg> {{view.name}}
|
||||
</svg><span> {{view.name}}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted" *ngIf='openDocuments.length > 0'>
|
||||
<ng-container i18n>Open documents</ng-container>
|
||||
<span i18n>Open documents</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column mb-2">
|
||||
<li class="nav-item w-100" *ngFor='let d of openDocuments'>
|
||||
<a class="nav-link text-truncate" routerLink="documents/{{d.id}}" routerLinkActive="active" (click)="closeMenu()">
|
||||
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="documents/{{d.id}}" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="d.title | documentTitle" [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
|
||||
</svg> {{d.title | documentTitle}}
|
||||
</svg><span> {{d.title | documentTitle}}</span>
|
||||
<span class="close" (click)="closeDocument(d); $event.preventDefault()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-x" viewBox="0 0 16 16">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
@@ -101,95 +112,96 @@
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item w-100" *ngIf="openDocuments.length >= 1">
|
||||
<a class="nav-link text-truncate" [routerLink]="[]" (click)="closeAll()">
|
||||
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" [routerLink]="[]" (click)="closeAll()" ngbPopover="Close all" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg> <ng-container i18n>Close all</ng-container>
|
||||
</svg><span> <ng-container i18n>Close all</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted">
|
||||
<ng-container i18n>Manage</ng-container>
|
||||
<span i18n>Manage</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column mb-2">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()">
|
||||
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Correspondents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#person"/>
|
||||
</svg> <ng-container i18n>Correspondents</ng-container>
|
||||
</svg><span> <ng-container i18n>Correspondents</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()">
|
||||
<li class="nav-item" tourAnchor="tour.tags">
|
||||
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Tags" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#tags"/>
|
||||
</svg> <ng-container i18n>Tags</ng-container>
|
||||
</svg><span> <ng-container i18n>Tags</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()">
|
||||
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Document types" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#hash"/>
|
||||
</svg> <ng-container i18n>Document types</ng-container>
|
||||
</svg><span> <ng-container i18n>Document types</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()">
|
||||
<a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Storage paths" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#folder"/>
|
||||
</svg> <ng-container i18n>Storage paths</ng-container>
|
||||
</svg><span> <ng-container i18n>Storage paths</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()">
|
||||
<li class="nav-item" tourAnchor="tour.file-tasks">
|
||||
<a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()" ngbPopover="File Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<span *ngIf="tasksService.failedFileTasks.length > 0 && slimSidebarEnabled" class="badge bg-danger position-absolute top-0 end-0">{{tasksService.failedFileTasks.length}}</span>
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#list-task"/>
|
||||
</svg> <ng-container i18n>File Tasks<ng-container *ngIf="tasksService.failedFileTasks.length > 0"><span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span></ng-container></ng-container>
|
||||
</svg><span> <ng-container i18n>File Tasks<span *ngIf="tasksService.failedFileTasks.length > 0"><span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span></span></ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()">
|
||||
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Logs" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#text-left"/>
|
||||
</svg> <ng-container i18n>Logs</ng-container>
|
||||
</svg><span> <ng-container i18n>Logs</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()">
|
||||
<li class="nav-item" tourAnchor="tour.settings">
|
||||
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Settings" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
|
||||
</svg> <ng-container i18n>Settings</ng-container>
|
||||
</svg><span> <ng-container i18n>Settings</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="admin/">
|
||||
<li class="nav-item" tourAnchor="tour.admin">
|
||||
<a class="nav-link" href="admin/" ngbPopover="Admin" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#toggles"/>
|
||||
</svg> <ng-container i18n>Admin</ng-container>
|
||||
</svg><span> <ng-container i18n>Admin</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h6 class="sidebar-heading px-3 mt-auto pt-4 mb-1 text-muted">
|
||||
<ng-container i18n>Info</ng-container>
|
||||
<span i18n>Info</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column mb-2">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" target="_blank" rel="noopener noreferrer" href="https://paperless-ngx.readthedocs.io/en/latest/">
|
||||
<li class="nav-item" tourAnchor="tour.outro">
|
||||
<a class="nav-link" target="_blank" rel="noopener noreferrer" href="https://paperless-ngx.readthedocs.io/en/latest/" ngbPopover="Documentation" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#question-circle"/>
|
||||
</svg> <ng-container i18n>Documentation</ng-container>
|
||||
</svg><span> <ng-container i18n>Documentation</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<div class="d-flex w-100 flex-wrap">
|
||||
<a class="nav-link pe-2 pb-1" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx">
|
||||
<a class="nav-link pe-2 pb-1" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx" ngbPopover="GitHub" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="sidebaricon" viewBox="0 0 16 16">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#github" />
|
||||
</svg> <ng-container i18n>GitHub</ng-container>
|
||||
</svg><span> <ng-container i18n>GitHub</ng-container></span>
|
||||
</a>
|
||||
<a class="nav-link-additional small text-muted ms-3" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx/discussions/categories/feature-requests" title="Suggest an idea" i18n-title>
|
||||
<a class="nav-link-additional small text-muted ms-3" [class.visually-hidden]="slimSidebarEnabled" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx/discussions/categories/feature-requests" title="Suggest an idea" i18n-title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1.1em" height="1.1em" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#lightbulb" />
|
||||
</svg>
|
||||
@@ -197,17 +209,28 @@
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item mt-2">
|
||||
<li class="nav-item mt-2" [class.visually-hidden]="slimSidebarEnabled">
|
||||
<div class="px-3 py-2 text-muted small d-flex align-items-center flex-wrap">
|
||||
<div class="me-3">{{ versionString }}</div>
|
||||
<div *ngIf="appRemoteVersion" class="version-check">
|
||||
<div *ngIf="!settingsService.updateCheckingIsSet || appRemoteVersion" class="version-check">
|
||||
<ng-template #updateAvailablePopContent>
|
||||
<span class="small">Paperless-ngx {{ appRemoteVersion.version }} <ng-container i18n>is available.</ng-container><br/><ng-container i18n>Click to view.</ng-container></span>
|
||||
</ng-template>
|
||||
<ng-template #updateCheckingNotEnabledPopContent>
|
||||
<span class="small"><ng-container i18n>Checking for updates is disabled.</ng-container><br/><ng-container i18n>Click for more information.</ng-container></span>
|
||||
<p class="small mb-2">
|
||||
<ng-container i18n>Paperless-ngx can automatically check for updates</ng-container>
|
||||
</p>
|
||||
<div class="btn-group btn-group-xs flex-fill w-100">
|
||||
<button class="btn btn-outline-primary" (click)="setUpdateChecking(true)">Enable</button>
|
||||
<button class="btn btn-outline-secondary" (click)="setUpdateChecking(false)">Disable</button>
|
||||
</div>
|
||||
<p class="small mb-0 mt-2">
|
||||
<a class="small text-decoration-none fst-italic" routerLink="/settings" fragment="update-checking" i18n>
|
||||
How does this work?
|
||||
</a>
|
||||
</p>
|
||||
</ng-template>
|
||||
<ng-container *ngIf="appRemoteVersion.feature_is_set; else updateCheckNotSet">
|
||||
<ng-container *ngIf="settingsService.updateCheckingIsSet; else updateCheckNotSet">
|
||||
<a *ngIf="appRemoteVersion.update_available" class="small text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx/releases"
|
||||
[ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave" container="body">
|
||||
<svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16">
|
||||
@@ -217,8 +240,8 @@
|
||||
</a>
|
||||
</ng-container>
|
||||
<ng-template #updateCheckNotSet>
|
||||
<a class="small text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://paperless-ngx.readthedocs.io/en/latest/configuration.html#update-checking"
|
||||
[ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter:mouseleave" container="body">
|
||||
<a class="small text-decoration-none" routerLink="/settings" fragment="update-checking"
|
||||
[ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter" container="body">
|
||||
<svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#info-circle" />
|
||||
</svg>
|
||||
@@ -231,7 +254,7 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main role="main" class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
||||
<main role="main" class="ms-sm-auto px-md-4" [ngClass]="slimSidebarEnabled ? 'col-slim' : 'col-md-9 col-lg-10'">
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
</div>
|
||||
|
@@ -1,3 +1,6 @@
|
||||
@import "node_modules/bootstrap/scss/functions";
|
||||
@import "node_modules/bootstrap/scss/variables";
|
||||
|
||||
/*
|
||||
* Sidebar
|
||||
*/
|
||||
@@ -14,6 +17,17 @@
|
||||
width: 0.8em;
|
||||
height: 0.8em;
|
||||
}
|
||||
|
||||
// These come from the col-md-3 col-lg-2 classes for regular sidebar, needed for animation
|
||||
@media (min-width: 768px) {
|
||||
max-width: 25%;
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
max-width: 16.66666667%;
|
||||
}
|
||||
|
||||
transition: all .2s ease;
|
||||
}
|
||||
@media (max-width: 767.98px) {
|
||||
.sidebar {
|
||||
@@ -21,6 +35,90 @@
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
transition: all .2s ease;
|
||||
}
|
||||
|
||||
.sidebar-slim-toggler {
|
||||
display: none; // hide on mobile
|
||||
}
|
||||
|
||||
.sidebar li.nav-item span,
|
||||
.sidebar .sidebar-heading span {
|
||||
transition: all .1s ease;
|
||||
}
|
||||
|
||||
@media(min-width: 768px) {
|
||||
.sidebar.slim {
|
||||
max-width: 50px;
|
||||
|
||||
li.nav-item span.badge {
|
||||
display: inline-block;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar.slim:not(.animating) {
|
||||
li.nav-item span,
|
||||
.sidebar-heading span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar.animating {
|
||||
li.nav-item span,
|
||||
.sidebar-heading span {
|
||||
display: unset;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar:not(.slim):not(.animating) {
|
||||
li.nav-item span,
|
||||
.sidebar-heading span {
|
||||
position: unset;
|
||||
opacity: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar.slim,
|
||||
.sidebar.animating {
|
||||
.text-truncate {
|
||||
text-overflow: unset !important;
|
||||
word-wrap: break-word !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar.slim {
|
||||
li.nav-item span.badge {
|
||||
display: inline-block;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.col-slim {
|
||||
padding-left: calc(50px + $grid-gutter-width) !important;
|
||||
}
|
||||
|
||||
.sidebar-slim-toggler {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: -12px;
|
||||
top: 60px;
|
||||
z-index: 996;
|
||||
--bs-btn-padding-x: 0.35rem;
|
||||
--bs-btn-padding-y: 0.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .popover-slim .popover-body {
|
||||
--bs-popover-body-padding-x: .5rem;
|
||||
--bs-popover-body-padding-y: .5rem;
|
||||
}
|
||||
|
||||
.sidebar-sticky {
|
||||
position: relative;
|
||||
top: 0;
|
||||
@@ -77,7 +175,7 @@
|
||||
|
||||
.close {
|
||||
display: none;
|
||||
position: absolute;
|
||||
position: absolute !important;
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
top: 0;
|
||||
@@ -145,17 +243,18 @@
|
||||
|
||||
form {
|
||||
position: relative;
|
||||
|
||||
> svg {
|
||||
position: absolute;
|
||||
left: 0.6rem;
|
||||
top: 0.5rem;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
position: absolute;
|
||||
left: 0.6rem;
|
||||
top: 0.5rem;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
svg {
|
||||
form > svg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, HostListener } from '@angular/core'
|
||||
import { Component, HostListener, OnInit } from '@angular/core'
|
||||
import { FormControl } from '@angular/forms'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { from, Observable } from 'rxjs'
|
||||
@@ -24,13 +24,15 @@ import {
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { TasksService } from 'src/app/services/tasks.service'
|
||||
import { ComponentCanDeactivate } from 'src/app/guards/dirty-doc.guard'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-app-frame',
|
||||
templateUrl: './app-frame.component.html',
|
||||
styleUrls: ['./app-frame.component.scss'],
|
||||
})
|
||||
export class AppFrameComponent implements ComponentCanDeactivate {
|
||||
export class AppFrameComponent implements OnInit, ComponentCanDeactivate {
|
||||
constructor(
|
||||
public router: Router,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
@@ -40,14 +42,15 @@ export class AppFrameComponent implements ComponentCanDeactivate {
|
||||
private remoteVersionService: RemoteVersionService,
|
||||
private list: DocumentListViewService,
|
||||
public settingsService: SettingsService,
|
||||
public tasksService: TasksService
|
||||
) {
|
||||
this.remoteVersionService
|
||||
.checkForUpdates()
|
||||
.subscribe((appRemoteVersion: AppRemoteVersion) => {
|
||||
this.appRemoteVersion = appRemoteVersion
|
||||
})
|
||||
tasksService.reload()
|
||||
public tasksService: TasksService,
|
||||
private readonly toastService: ToastService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.settingsService.get(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED)) {
|
||||
this.checkForUpdates()
|
||||
}
|
||||
this.tasksService.reload()
|
||||
}
|
||||
|
||||
versionString = `${environment.appTitle} ${environment.version}`
|
||||
@@ -55,12 +58,55 @@ export class AppFrameComponent implements ComponentCanDeactivate {
|
||||
|
||||
isMenuCollapsed: boolean = true
|
||||
|
||||
slimSidebarAnimating: boolean = false
|
||||
|
||||
toggleSlimSidebar(): void {
|
||||
this.slimSidebarAnimating = true
|
||||
this.slimSidebarEnabled = !this.slimSidebarEnabled
|
||||
setTimeout(() => {
|
||||
this.slimSidebarAnimating = false
|
||||
}, 200) // slightly longer than css animation for slim sidebar
|
||||
}
|
||||
|
||||
get slimSidebarEnabled(): boolean {
|
||||
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
|
||||
}
|
||||
|
||||
set slimSidebarEnabled(enabled: boolean) {
|
||||
this.settingsService.set(SETTINGS_KEYS.SLIM_SIDEBAR, enabled)
|
||||
this.settingsService
|
||||
.storeSettings()
|
||||
.pipe(first())
|
||||
.subscribe({
|
||||
error: (error) => {
|
||||
this.toastService.showError(
|
||||
$localize`An error occurred while saving settings.`
|
||||
)
|
||||
console.log(error)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
closeMenu() {
|
||||
this.isMenuCollapsed = true
|
||||
}
|
||||
|
||||
searchField = new FormControl('')
|
||||
|
||||
get searchFieldEmpty(): boolean {
|
||||
return this.searchField.value.trim().length == 0
|
||||
}
|
||||
|
||||
resetSearchField() {
|
||||
this.searchField.reset('')
|
||||
}
|
||||
|
||||
searchFieldKeyup(event: KeyboardEvent) {
|
||||
if (event.key == 'Escape') {
|
||||
this.resetSearchField()
|
||||
}
|
||||
}
|
||||
|
||||
get openDocuments(): PaperlessDocument[] {
|
||||
return this.openDocumentsService.getOpenDocuments()
|
||||
}
|
||||
@@ -150,4 +196,30 @@ export class AppFrameComponent implements ComponentCanDeactivate {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private checkForUpdates() {
|
||||
this.remoteVersionService
|
||||
.checkForUpdates()
|
||||
.subscribe((appRemoteVersion: AppRemoteVersion) => {
|
||||
this.appRemoteVersion = appRemoteVersion
|
||||
})
|
||||
}
|
||||
|
||||
setUpdateChecking(enable: boolean) {
|
||||
this.settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, enable)
|
||||
this.settingsService
|
||||
.storeSettings()
|
||||
.pipe(first())
|
||||
.subscribe({
|
||||
error: (error) => {
|
||||
this.toastService.showError(
|
||||
$localize`An error occurred while saving update checking settings.`
|
||||
)
|
||||
console.log(error)
|
||||
},
|
||||
})
|
||||
if (enable) {
|
||||
this.checkForUpdates()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,9 @@
|
||||
<button *ngIf="active" class="position-absolute top-0 start-100 translate-middle badge bg-secondary border border-light rounded-pill p-1" title="Clear" i18n-title (click)="onClick($event)">
|
||||
<svg *ngIf="!isNumbered && selected" width="1em" height="1em" class="check m-0 p-0 opacity-75" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#check-lg"/>
|
||||
</svg>
|
||||
<div *ngIf="isNumbered" class="number">{{number}}<span class="visually-hidden">selected</span></div>
|
||||
<svg width=".9em" height="1em" class="x m-0 p-0 opacity-75" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x-lg"/>
|
||||
</svg>
|
||||
</button>
|
@@ -0,0 +1,28 @@
|
||||
.badge {
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.x {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.number {
|
||||
min-width: 1em;
|
||||
min-height: 1em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
.check,
|
||||
.number {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
.x {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: calc(50% - 4px);
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'app-clearable-badge',
|
||||
templateUrl: './clearable-badge.component.html',
|
||||
styleUrls: ['./clearable-badge.component.scss'],
|
||||
})
|
||||
export class ClearableBadge {
|
||||
constructor() {}
|
||||
|
||||
@Input()
|
||||
number: number
|
||||
|
||||
@Input()
|
||||
selected: boolean
|
||||
|
||||
@Output()
|
||||
cleared: EventEmitter<boolean> = new EventEmitter()
|
||||
|
||||
get active(): boolean {
|
||||
return this.selected || this.number > -1
|
||||
}
|
||||
|
||||
get isNumbered(): boolean {
|
||||
return this.number > -1
|
||||
}
|
||||
|
||||
onClick(event: PointerEvent) {
|
||||
this.cleared.emit(true)
|
||||
event.stopImmediatePropagation()
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
@@ -16,4 +16,7 @@
|
||||
<ngb-progressbar *ngIf="!confirmButtonEnabled" style="height: 1px;" type="dark" [max]="secondsTotal" [value]="seconds"></ngb-progressbar>
|
||||
<span class="visually-hidden">{{ seconds | number: '1.0-0' }} seconds</span>
|
||||
</button>
|
||||
<button *ngIf="alternativeBtnCaption" type="button" class="btn" [class]="alternativeBtnClass" (click)="alternative()" [disabled]="!alternativeButtonEnabled || !buttonsEnabled">
|
||||
{{alternativeBtnCaption}}
|
||||
</button>
|
||||
</div>
|
||||
|
@@ -13,6 +13,9 @@ export class ConfirmDialogComponent {
|
||||
@Output()
|
||||
public confirmClicked = new EventEmitter()
|
||||
|
||||
@Output()
|
||||
public alternativeClicked = new EventEmitter()
|
||||
|
||||
@Input()
|
||||
title = $localize`Confirmation`
|
||||
|
||||
@@ -28,14 +31,22 @@ export class ConfirmDialogComponent {
|
||||
@Input()
|
||||
btnCaption = $localize`Confirm`
|
||||
|
||||
@Input()
|
||||
alternativeBtnClass = 'btn-secondary'
|
||||
|
||||
@Input()
|
||||
alternativeBtnCaption
|
||||
|
||||
@Input()
|
||||
buttonsEnabled = true
|
||||
|
||||
confirmButtonEnabled = true
|
||||
alternativeButtonEnabled = true
|
||||
seconds = 0
|
||||
secondsTotal = 0
|
||||
|
||||
confirmSubject: Subject<boolean>
|
||||
alternativeSubject: Subject<boolean>
|
||||
|
||||
delayConfirm(seconds: number) {
|
||||
const refreshInterval = 0.15 // s
|
||||
@@ -68,4 +79,10 @@ export class ConfirmDialogComponent {
|
||||
this.confirmSubject?.next(true)
|
||||
this.confirmSubject?.complete()
|
||||
}
|
||||
|
||||
alternative() {
|
||||
this.alternativeClicked.emit()
|
||||
this.alternativeSubject?.next(true)
|
||||
this.alternativeSubject?.complete()
|
||||
}
|
||||
}
|
||||
|
@@ -1,11 +1,17 @@
|
||||
<div class="btn-group w-100" ngbDropdown role="group">
|
||||
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="dateBefore || dateAfter ? 'btn-primary' : 'btn-outline-primary'">
|
||||
{{title}}
|
||||
<app-clearable-badge [selected]="isActive" (cleared)="reset()"></app-clearable-badge><span class="visually-hidden">selected</span>
|
||||
</button>
|
||||
<div class="dropdown-menu date-dropdown shadow pt-0" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
|
||||
<div class="list-group list-group-flush">
|
||||
<button *ngFor="let qf of quickFilters" class="list-group-item small list-goup list-group-item-action d-flex p-2 ps-3" role="menuitem" (click)="setDateQuickFilter(qf.id)">
|
||||
{{qf.name}}
|
||||
<button *ngFor="let rd of relativeDates" class="list-group-item small list-goup list-group-item-action d-flex p-2" role="menuitem" (click)="setRelativeDate(rd.date)">
|
||||
<div _ngcontent-hga-c166="" class="selected-icon me-1">
|
||||
<svg *ngIf="relativeDate === rd.date" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16">
|
||||
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
|
||||
</svg>
|
||||
</div>
|
||||
{{rd.name}}
|
||||
</button>
|
||||
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
|
||||
|
||||
|
@@ -5,3 +5,8 @@
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.selected-icon {
|
||||
min-width: 1em;
|
||||
min-height: 1em;
|
||||
}
|
||||
|
@@ -16,12 +16,15 @@ import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
|
||||
export interface DateSelection {
|
||||
before?: string
|
||||
after?: string
|
||||
relativeDateID?: number
|
||||
}
|
||||
|
||||
const LAST_7_DAYS = 0
|
||||
const LAST_MONTH = 1
|
||||
const LAST_3_MONTHS = 2
|
||||
const LAST_YEAR = 3
|
||||
export enum RelativeDate {
|
||||
LAST_7_DAYS = 0,
|
||||
LAST_MONTH = 1,
|
||||
LAST_3_MONTHS = 2,
|
||||
LAST_YEAR = 3,
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-date-dropdown',
|
||||
@@ -34,11 +37,23 @@ export class DateDropdownComponent implements OnInit, OnDestroy {
|
||||
this.datePlaceHolder = settings.getLocalizedDateInputFormat()
|
||||
}
|
||||
|
||||
quickFilters = [
|
||||
{ id: LAST_7_DAYS, name: $localize`Last 7 days` },
|
||||
{ id: LAST_MONTH, name: $localize`Last month` },
|
||||
{ id: LAST_3_MONTHS, name: $localize`Last 3 months` },
|
||||
{ id: LAST_YEAR, name: $localize`Last year` },
|
||||
relativeDates = [
|
||||
{
|
||||
date: RelativeDate.LAST_7_DAYS,
|
||||
name: $localize`Last 7 days`,
|
||||
},
|
||||
{
|
||||
date: RelativeDate.LAST_MONTH,
|
||||
name: $localize`Last month`,
|
||||
},
|
||||
{
|
||||
date: RelativeDate.LAST_3_MONTHS,
|
||||
name: $localize`Last 3 months`,
|
||||
},
|
||||
{
|
||||
date: RelativeDate.LAST_YEAR,
|
||||
name: $localize`Last year`,
|
||||
},
|
||||
]
|
||||
|
||||
datePlaceHolder: string
|
||||
@@ -55,12 +70,26 @@ export class DateDropdownComponent implements OnInit, OnDestroy {
|
||||
@Output()
|
||||
dateAfterChange = new EventEmitter<string>()
|
||||
|
||||
@Input()
|
||||
relativeDate: RelativeDate
|
||||
|
||||
@Output()
|
||||
relativeDateChange = new EventEmitter<number>()
|
||||
|
||||
@Input()
|
||||
title: string
|
||||
|
||||
@Output()
|
||||
datesSet = new EventEmitter<DateSelection>()
|
||||
|
||||
get isActive(): boolean {
|
||||
return (
|
||||
this.relativeDate !== null ||
|
||||
this.dateAfter?.length > 0 ||
|
||||
this.dateBefore?.length > 0
|
||||
)
|
||||
}
|
||||
|
||||
private datesSetDebounce$ = new Subject()
|
||||
|
||||
private sub: Subscription
|
||||
@@ -77,37 +106,33 @@ export class DateDropdownComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
setDateQuickFilter(qf: number) {
|
||||
reset() {
|
||||
this.dateBefore = null
|
||||
let date = new Date()
|
||||
switch (qf) {
|
||||
case LAST_7_DAYS:
|
||||
date.setDate(date.getDate() - 7)
|
||||
break
|
||||
this.dateAfter = null
|
||||
this.relativeDate = null
|
||||
this.onChange()
|
||||
}
|
||||
|
||||
case LAST_MONTH:
|
||||
date.setMonth(date.getMonth() - 1)
|
||||
break
|
||||
|
||||
case LAST_3_MONTHS:
|
||||
date.setMonth(date.getMonth() - 3)
|
||||
break
|
||||
|
||||
case LAST_YEAR:
|
||||
date.setFullYear(date.getFullYear() - 1)
|
||||
break
|
||||
}
|
||||
this.dateAfter = formatDate(date, 'yyyy-MM-dd', 'en-us', 'UTC')
|
||||
setRelativeDate(rd: RelativeDate) {
|
||||
this.dateBefore = null
|
||||
this.dateAfter = null
|
||||
this.relativeDate = this.relativeDate == rd ? null : rd
|
||||
this.onChange()
|
||||
}
|
||||
|
||||
onChange() {
|
||||
this.dateAfterChange.emit(this.dateAfter)
|
||||
this.dateBeforeChange.emit(this.dateBefore)
|
||||
this.datesSet.emit({ after: this.dateAfter, before: this.dateBefore })
|
||||
this.dateAfterChange.emit(this.dateAfter)
|
||||
this.relativeDateChange.emit(this.relativeDate)
|
||||
this.datesSet.emit({
|
||||
after: this.dateAfter,
|
||||
before: this.dateBefore,
|
||||
relativeDateID: this.relativeDate,
|
||||
})
|
||||
}
|
||||
|
||||
onChangeDebounce() {
|
||||
this.relativeDate = null
|
||||
this.datesSetDebounce$.next({
|
||||
after: this.dateAfter,
|
||||
before: this.dateBefore,
|
||||
|
@@ -2,6 +2,7 @@ import { Component } from '@angular/core'
|
||||
import { FormControl, FormGroup } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
@@ -31,7 +32,7 @@ export class CorrespondentEditDialogComponent extends EditDialogComponent<Paperl
|
||||
getForm(): FormGroup {
|
||||
return new FormGroup({
|
||||
name: new FormControl(''),
|
||||
matching_algorithm: new FormControl(1),
|
||||
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
||||
match: new FormControl(''),
|
||||
is_insensitive: new FormControl(true),
|
||||
})
|
||||
|
@@ -2,6 +2,7 @@ import { Component } from '@angular/core'
|
||||
import { FormControl, FormGroup } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
|
||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
@@ -31,7 +32,7 @@ export class DocumentTypeEditDialogComponent extends EditDialogComponent<Paperle
|
||||
getForm(): FormGroup {
|
||||
return new FormGroup({
|
||||
name: new FormControl(''),
|
||||
matching_algorithm: new FormControl(1),
|
||||
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
||||
match: new FormControl(''),
|
||||
is_insensitive: new FormControl(true),
|
||||
})
|
||||
|
@@ -2,6 +2,7 @@ import { Component } from '@angular/core'
|
||||
import { FormControl, FormGroup } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
@@ -42,7 +43,7 @@ export class StoragePathEditDialogComponent extends EditDialogComponent<Paperles
|
||||
return new FormGroup({
|
||||
name: new FormControl(''),
|
||||
path: new FormControl(''),
|
||||
matching_algorithm: new FormControl(1),
|
||||
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
||||
match: new FormControl(''),
|
||||
is_insensitive: new FormControl(true),
|
||||
})
|
||||
|
@@ -6,6 +6,7 @@ import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||
import { TagService } from 'src/app/services/rest/tag.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { randomColor } from 'src/app/utils/color'
|
||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||
|
||||
@Component({
|
||||
selector: 'app-tag-edit-dialog',
|
||||
@@ -34,7 +35,7 @@ export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> {
|
||||
name: new FormControl(''),
|
||||
color: new FormControl(randomColor()),
|
||||
is_inbox_tag: new FormControl(false),
|
||||
matching_algorithm: new FormControl(1),
|
||||
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
||||
match: new FormControl(''),
|
||||
is_insensitive: new FormControl(true),
|
||||
})
|
||||
|
@@ -5,12 +5,7 @@
|
||||
</svg>
|
||||
<div class="d-none d-sm-inline"> {{title}}</div>
|
||||
<ng-container *ngIf="!editing && selectionModel.selectionSize() > 0">
|
||||
<div *ngIf="multiple" class="position-absolute top-0 start-100 translate-middle badge bg-secondary border border-light text-light rounded-pill">
|
||||
{{selectionModel.totalCount}}<span class="visually-hidden">selected</span>
|
||||
</div>
|
||||
<div *ngIf="!multiple" 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>
|
||||
<app-clearable-badge [number]="multiple ? selectionModel.totalCount : undefined" [selected]="!multiple && selectionModel.selectionSize() > 0" (cleared)="reset()"></app-clearable-badge>
|
||||
</ng-container>
|
||||
</button>
|
||||
<div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
|
||||
|
@@ -17,25 +17,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.btn-group-xs {
|
||||
> .btn {
|
||||
padding: 0.2rem 0.25rem;
|
||||
font-size: 0.675rem;
|
||||
line-height: 1.2;
|
||||
border-radius: 0.15rem;
|
||||
}
|
||||
|
||||
> .btn:not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
> .btn:not(:last-child) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-group > label.disabled {
|
||||
filter: brightness(0.5);
|
||||
|
||||
|
@@ -384,4 +384,9 @@ export class FilterableDropdownComponent {
|
||||
this.selectionModel.exclude(itemID)
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.selectionModel.reset()
|
||||
this.selectionModelChange.emit(this.selectionModel)
|
||||
}
|
||||
}
|
||||
|
@@ -19,17 +19,20 @@
|
||||
</svg>
|
||||
</app-page-header>
|
||||
|
||||
<div class='row'>
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<ng-container *ngIf="savedViewService.loading">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<ng-container i18n>Loading...</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<app-welcome-widget *ngIf="!savedViewService.loading && savedViewService.dashboardViews.length == 0"></app-welcome-widget>
|
||||
<app-welcome-widget *ngIf="settingsService.offerTour()" tourAnchor="tour.dashboard"></app-welcome-widget>
|
||||
|
||||
<ng-container *ngFor="let v of savedViewService.dashboardViews">
|
||||
<app-saved-view-widget [savedView]="v"></app-saved-view-widget>
|
||||
<ng-container *ngFor="let v of savedViewService.dashboardViews; first as isFirst">
|
||||
<app-saved-view-widget *ngIf="isFirst; else noTour" [savedView]="v" tourAnchor="tour.dashboard"></app-saved-view-widget>
|
||||
<ng-template #noTour>
|
||||
<app-saved-view-widget [savedView]="v"></app-saved-view-widget>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { Meta } from '@angular/platform-browser'
|
||||
import { Component } from '@angular/core'
|
||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
|
||||
@@ -16,9 +15,9 @@ export class DashboardComponent {
|
||||
|
||||
get subtitle() {
|
||||
if (this.settingsService.displayName) {
|
||||
return $localize`Hello ${this.settingsService.displayName}, welcome to Paperless-ngx!`
|
||||
return $localize`Hello ${this.settingsService.displayName}, welcome to Paperless-ngx`
|
||||
} else {
|
||||
return $localize`Welcome to Paperless-ngx!`
|
||||
return $localize`Welcome to Paperless-ngx`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div content>
|
||||
<div content tourAnchor="tour.upload-widget">
|
||||
<form>
|
||||
<ngx-file-drop dropZoneLabel="Drop documents here or" browseBtnLabel="Browse files" (onFileDrop)="dropped($event)"
|
||||
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" dropZoneClassName="bg-light card"
|
||||
|
@@ -1,16 +1,11 @@
|
||||
<app-widget-frame title="First steps" i18n-title>
|
||||
|
||||
<ng-container content>
|
||||
<img src="assets/save-filter.png" class="float-right">
|
||||
<p i18n>Paperless is running! :)</p>
|
||||
<p i18n>You can start uploading documents by dropping them in the file upload box to the right or by dropping them in the configured consumption folder and they'll start showing up in the documents list.
|
||||
After you've added some metadata to your documents, use the filtering mechanisms of paperless to create custom views (such as 'Recently added', 'Tagged TODO') and they will appear on the dashboard instead of this message.</p>
|
||||
<p i18n>Paperless offers some more features that try to make your life easier:</p>
|
||||
<ul>
|
||||
<li i18n>Once you've got a couple documents in paperless and added metadata to them, paperless can assign that metadata to new documents automatically.</li>
|
||||
<li i18n>You can configure paperless to read your mails and add documents from attached files.</li>
|
||||
</ul>
|
||||
<p i18n>Consult the documentation on how to use these features. The section on basic usage also has some information on how to use paperless in general.</p>
|
||||
</ng-container>
|
||||
|
||||
</app-widget-frame>
|
||||
<ngb-alert type="primary" [dismissible]="false">
|
||||
<!-- [dismissible]="isFinished(status)" (closed)="dismiss(status)" -->
|
||||
<h4 class="alert-heading"><ng-container i18n>Paperless-ngx is running!</ng-container> 🎉</h4>
|
||||
<p i18n>You're ready to start uploading documents! Explore the various features of this web app on your own, or start a quick tour using the button below.</p>
|
||||
<p i18n>More detail on how to use and configure Paperless-ngx is always available in the <a href="https://paperless-ngx.readthedocs.io" target="_blank">documentation</a>.</p>
|
||||
<hr>
|
||||
<div class="d-flex align-items-end">
|
||||
<p class="lead fs-6 m-0"><em i18n>Thanks for being a part of the Paperless-ngx community!</em></p>
|
||||
<button class="btn btn-primary ms-auto flex-shrink-0" (click)="tourService.start()"><ng-container i18n>Start the tour</ng-container> →</button>
|
||||
</div>
|
||||
</ngb-alert>
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||
|
||||
@Component({
|
||||
selector: 'app-welcome-widget',
|
||||
@@ -6,7 +7,7 @@ import { Component, OnInit } from '@angular/core'
|
||||
styleUrls: ['./welcome-widget.component.scss'],
|
||||
})
|
||||
export class WelcomeWidgetComponent implements OnInit {
|
||||
constructor() {}
|
||||
constructor(public readonly tourService: TourService) {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
}
|
||||
|
@@ -337,7 +337,7 @@ export class DocumentDetailComponent
|
||||
})
|
||||
)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(({ newStoragePath, documentTypes: storagePaths }) => {
|
||||
.subscribe(({ newStoragePath, storagePaths }) => {
|
||||
this.storagePaths = storagePaths.results
|
||||
this.documentForm.get('storage_path').setValue(newStoragePath.id)
|
||||
})
|
||||
|
@@ -60,14 +60,19 @@
|
||||
</div>
|
||||
|
||||
<div class="btn-group ms-2 flex-fill" ngbDropdown role="group">
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle flex-fill" ngbDropdownToggle i18n>Views</button>
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle flex-fill" tourAnchor="tour.documents-views" ngbDropdownToggle>
|
||||
<ng-container i18n>Views</ng-container>
|
||||
<div *ngIf="savedViewIsModified" class="position-absolute top-0 start-100 p-2 translate-middle badge bg-secondary border border-light rounded-circle">
|
||||
<span class="visually-hidden">selected</span>
|
||||
</div>
|
||||
</button>
|
||||
<div class="dropdown-menu shadow dropdown-menu-right" ngbDropdownMenu>
|
||||
<ng-container *ngIf="!list.activeSavedViewId">
|
||||
<button ngbDropdownItem *ngFor="let view of savedViewService.allViews" (click)="loadViewConfig(view.id)">{{view.name}}</button>
|
||||
<div class="dropdown-divider" *ngIf="savedViewService.allViews.length > 0"></div>
|
||||
</ng-container>
|
||||
|
||||
<button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.activeSavedViewId" i18n>Save "{{list.activeSavedViewTitle}}"</button>
|
||||
<button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.activeSavedViewId" [disabled]="!savedViewIsModified" i18n>Save "{{list.activeSavedViewTitle}}"</button>
|
||||
<button ngbDropdownItem (click)="saveViewConfigAs()" i18n>Save as...</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -79,6 +84,7 @@
|
||||
<app-bulk-editor [hidden]="!isBulkEditing"></app-bulk-editor>
|
||||
</div>
|
||||
|
||||
|
||||
<ng-template #pagination>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<p>
|
||||
@@ -96,14 +102,15 @@
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-container *ngTemplateOutlet="pagination"></ng-container>
|
||||
<div tourAnchor="tour.documents">
|
||||
<ng-container *ngTemplateOutlet="pagination"></ng-container>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="list.error ; else documentListNoError">
|
||||
<div class="alert alert-danger" role="alert"><ng-container i18n>Error while loading documents</ng-container>: {{list.error}}</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #documentListNoError>
|
||||
|
||||
<div *ngIf="displayMode == 'largeCards'">
|
||||
<app-document-card-large [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" *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>
|
||||
|
@@ -11,7 +11,7 @@ tr {
|
||||
}
|
||||
|
||||
$paperless-card-breakpoints: (
|
||||
0: 2, // xs
|
||||
// 0: 2, // xs is manual for slim-sidebar
|
||||
768px: 3, //md
|
||||
992px: 4, //lg
|
||||
1200px: 5, //xl
|
||||
@@ -22,6 +22,12 @@ $paperless-card-breakpoints: (
|
||||
);
|
||||
|
||||
.row-cols-paperless-cards {
|
||||
// xs, we dont want in .col-slim block
|
||||
> * {
|
||||
flex: 0 0 auto;
|
||||
width: calc(100% / 2);
|
||||
}
|
||||
|
||||
@each $width, $n_cols in $paperless-card-breakpoints {
|
||||
@media(min-width: $width) {
|
||||
> * {
|
||||
@@ -32,6 +38,17 @@ $paperless-card-breakpoints: (
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .col-slim .row-cols-paperless-cards {
|
||||
@each $width, $n_cols in $paperless-card-breakpoints {
|
||||
@media(min-width: $width) {
|
||||
> * {
|
||||
flex: 0 0 auto;
|
||||
width: calc(100% / ($n-cols + 1)) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu-right {
|
||||
right: 0 !important;
|
||||
left: auto !important;
|
||||
|
@@ -9,7 +9,11 @@ import {
|
||||
import { ActivatedRoute, convertToParamMap, Router } from '@angular/router'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { filter, first, map, Subject, switchMap, takeUntil } from 'rxjs'
|
||||
import { FilterRule, isFullTextFilterRule } from 'src/app/data/filter-rule'
|
||||
import {
|
||||
FilterRule,
|
||||
filterRulesDiffer,
|
||||
isFullTextFilterRule,
|
||||
} from 'src/app/data/filter-rule'
|
||||
import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type'
|
||||
import { PaperlessDocument } from 'src/app/data/paperless-document'
|
||||
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
|
||||
@@ -54,15 +58,36 @@ export class DocumentListComponent implements OnInit, OnDestroy {
|
||||
displayMode = 'smallCards' // largeCards, smallCards, details
|
||||
|
||||
unmodifiedFilterRules: FilterRule[] = []
|
||||
private unmodifiedSavedView: PaperlessSavedView
|
||||
|
||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||
|
||||
get savedViewIsModified(): boolean {
|
||||
if (!this.list.activeSavedViewId || !this.unmodifiedSavedView) return false
|
||||
else {
|
||||
return (
|
||||
this.unmodifiedSavedView.sort_field !== this.list.sortField ||
|
||||
this.unmodifiedSavedView.sort_reverse !== this.list.sortReverse ||
|
||||
filterRulesDiffer(
|
||||
this.unmodifiedSavedView.filter_rules,
|
||||
this.list.filterRules
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
get isFiltered() {
|
||||
return this.list.filterRules?.length > 0
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
return this.list.activeSavedViewTitle || $localize`Documents`
|
||||
let title = this.list.activeSavedViewTitle
|
||||
if (title && this.savedViewIsModified) {
|
||||
title += '*'
|
||||
} else if (!title) {
|
||||
title = $localize`Documents`
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
getSortFields() {
|
||||
@@ -122,7 +147,7 @@ export class DocumentListComponent implements OnInit, OnDestroy {
|
||||
this.router.navigate(['404'])
|
||||
return
|
||||
}
|
||||
|
||||
this.unmodifiedSavedView = view
|
||||
this.list.activateSavedViewWithQueryParams(
|
||||
view,
|
||||
convertToParamMap(this.route.snapshot.queryParams)
|
||||
@@ -139,13 +164,7 @@ export class DocumentListComponent implements OnInit, OnDestroy {
|
||||
.subscribe((queryParams) => {
|
||||
if (queryParams.has('view')) {
|
||||
// loading a saved view on /documents
|
||||
this.savedViewService
|
||||
.getCached(parseInt(queryParams.get('view')))
|
||||
.pipe(first())
|
||||
.subscribe((view) => {
|
||||
this.list.activateSavedView(view)
|
||||
this.list.reload()
|
||||
})
|
||||
this.loadViewConfig(parseInt(queryParams.get('view')))
|
||||
} else {
|
||||
this.list.activateSavedView(null)
|
||||
this.list.loadFromQueryParams(queryParams)
|
||||
@@ -171,7 +190,8 @@ export class DocumentListComponent implements OnInit, OnDestroy {
|
||||
this.savedViewService
|
||||
.patch(savedView)
|
||||
.pipe(first())
|
||||
.subscribe((result) => {
|
||||
.subscribe((view) => {
|
||||
this.unmodifiedSavedView = view
|
||||
this.toastService.showInfo(
|
||||
$localize`View "${this.list.activeSavedViewTitle}" saved successfully.`
|
||||
)
|
||||
@@ -180,6 +200,17 @@ export class DocumentListComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
loadViewConfig(viewID: number) {
|
||||
this.savedViewService
|
||||
.getCached(viewID)
|
||||
.pipe(first())
|
||||
.subscribe((view) => {
|
||||
this.unmodifiedSavedView = view
|
||||
this.list.activateSavedView(view)
|
||||
this.list.reload()
|
||||
})
|
||||
}
|
||||
|
||||
saveViewConfigAs() {
|
||||
let modal = this.modalService.open(SaveViewConfigDialogComponent, {
|
||||
backdrop: 'static',
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<div class="row flex-wrap">
|
||||
<div class="row flex-wrap" tourAnchor="tour.documents-filter-editor">
|
||||
<div class="col mb-2 mb-xxl-0">
|
||||
<div class="form-inline d-flex align-items-center">
|
||||
<div class="input-group input-group-sm flex-fill w-auto flex-nowrap">
|
||||
@@ -11,7 +11,12 @@
|
||||
<select *ngIf="textFilterTarget == 'asn'" class="form-select flex-grow-0 w-auto" [(ngModel)]="textFilterModifier" (change)="textFilterModifierChange()">
|
||||
<option *ngFor="let m of textFilterModifiers" ngbDropdownItem [value]="m.id">{{m.label}}</option>
|
||||
</select>
|
||||
<input #textFilterInput class="form-control form-control-sm" type="text" [disabled]="textFilterModifierIsNull" [(ngModel)]="textFilter" (keyup.enter)="textFilterEnter()" [readonly]="textFilterTarget == 'fulltext-morelike'">
|
||||
<button *ngIf="_textFilter" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0 z-10" (click)="resetTextField()">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x me-1" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<input #textFilterInput class="form-control form-control-sm" type="text" [disabled]="textFilterModifierIsNull" [(ngModel)]="textFilter" (keyup)="textFilterKeyup($event)" [readonly]="textFilterTarget == 'fulltext-morelike'">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,12 +59,14 @@
|
||||
title="Created" i18n-title
|
||||
(datesSet)="updateRules()"
|
||||
[(dateBefore)]="dateCreatedBefore"
|
||||
[(dateAfter)]="dateCreatedAfter"></app-date-dropdown>
|
||||
[(dateAfter)]="dateCreatedAfter"
|
||||
[(relativeDate)]="dateCreatedRelativeDate"></app-date-dropdown>
|
||||
<app-date-dropdown class="mb-2 mb-xl-0"
|
||||
title="Added" i18n-title
|
||||
(datesSet)="updateRules()"
|
||||
[(dateBefore)]="dateAddedBefore"
|
||||
[(dateAfter)]="dateAddedAfter"
|
||||
title="Added" i18n-title
|
||||
(datesSet)="updateRules()"></app-date-dropdown>
|
||||
[(relativeDate)]="dateAddedRelativeDate"></app-date-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -21,3 +21,7 @@
|
||||
input[type="text"] {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.z-10 {
|
||||
z-index: 10;
|
||||
}
|
||||
|
@@ -44,6 +44,7 @@ import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { PaperlessDocument } from 'src/app/data/paperless-document'
|
||||
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||
import { RelativeDate } from '../../common/date-dropdown/date-dropdown.component'
|
||||
|
||||
const TEXT_FILTER_TARGET_TITLE = 'title'
|
||||
const TEXT_FILTER_TARGET_TITLE_CONTENT = 'title-content'
|
||||
@@ -57,6 +58,27 @@ const TEXT_FILTER_MODIFIER_NOTNULL = 'not null'
|
||||
const TEXT_FILTER_MODIFIER_GT = 'greater'
|
||||
const TEXT_FILTER_MODIFIER_LT = 'less'
|
||||
|
||||
const RELATIVE_DATE_QUERY_REGEXP_CREATED = /created:\[([^\]]+)\]/g
|
||||
const RELATIVE_DATE_QUERY_REGEXP_ADDED = /added:\[([^\]]+)\]/g
|
||||
const RELATIVE_DATE_QUERYSTRINGS = [
|
||||
{
|
||||
relativeDate: RelativeDate.LAST_7_DAYS,
|
||||
dateQuery: '-1 week to now',
|
||||
},
|
||||
{
|
||||
relativeDate: RelativeDate.LAST_MONTH,
|
||||
dateQuery: '-1 month to now',
|
||||
},
|
||||
{
|
||||
relativeDate: RelativeDate.LAST_3_MONTHS,
|
||||
dateQuery: '-3 month to now',
|
||||
},
|
||||
{
|
||||
relativeDate: RelativeDate.LAST_YEAR,
|
||||
dateQuery: '-1 year to now',
|
||||
},
|
||||
]
|
||||
|
||||
@Component({
|
||||
selector: 'app-filter-editor',
|
||||
templateUrl: './filter-editor.component.html',
|
||||
@@ -197,6 +219,8 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
dateCreatedAfter: string
|
||||
dateAddedBefore: string
|
||||
dateAddedAfter: string
|
||||
dateCreatedRelativeDate: RelativeDate
|
||||
dateAddedRelativeDate: RelativeDate
|
||||
|
||||
_unmodifiedFilterRules: FilterRule[] = []
|
||||
_filterRules: FilterRule[] = []
|
||||
@@ -228,6 +252,8 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
this.dateAddedAfter = null
|
||||
this.dateCreatedBefore = null
|
||||
this.dateCreatedAfter = null
|
||||
this.dateCreatedRelativeDate = null
|
||||
this.dateAddedRelativeDate = null
|
||||
this.textFilterModifier = TEXT_FILTER_MODIFIER_EQUALS
|
||||
|
||||
value.forEach((rule) => {
|
||||
@@ -245,8 +271,39 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
this.textFilterTarget = TEXT_FILTER_TARGET_ASN
|
||||
break
|
||||
case FILTER_FULLTEXT_QUERY:
|
||||
this._textFilter = rule.value
|
||||
this.textFilterTarget = TEXT_FILTER_TARGET_FULLTEXT_QUERY
|
||||
let allQueryArgs = rule.value.split(',')
|
||||
let textQueryArgs = []
|
||||
allQueryArgs.forEach((arg) => {
|
||||
if (arg.match(RELATIVE_DATE_QUERY_REGEXP_CREATED)) {
|
||||
;[...arg.matchAll(RELATIVE_DATE_QUERY_REGEXP_CREATED)].forEach(
|
||||
(match) => {
|
||||
if (match[1]?.length) {
|
||||
this.dateCreatedRelativeDate =
|
||||
RELATIVE_DATE_QUERYSTRINGS.find(
|
||||
(qS) => qS.dateQuery == match[1]
|
||||
)?.relativeDate
|
||||
}
|
||||
}
|
||||
)
|
||||
} else if (arg.match(RELATIVE_DATE_QUERY_REGEXP_ADDED)) {
|
||||
;[...arg.matchAll(RELATIVE_DATE_QUERY_REGEXP_ADDED)].forEach(
|
||||
(match) => {
|
||||
if (match[1]?.length) {
|
||||
this.dateAddedRelativeDate =
|
||||
RELATIVE_DATE_QUERYSTRINGS.find(
|
||||
(qS) => qS.dateQuery == match[1]
|
||||
)?.relativeDate
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
textQueryArgs.push(arg)
|
||||
}
|
||||
})
|
||||
if (textQueryArgs.length) {
|
||||
this._textFilter = textQueryArgs.join(',')
|
||||
this.textFilterTarget = TEXT_FILTER_TARGET_FULLTEXT_QUERY
|
||||
}
|
||||
break
|
||||
case FILTER_FULLTEXT_MORELIKE:
|
||||
this._moreLikeId = +rule.value
|
||||
@@ -471,6 +528,89 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
value: this.dateAddedAfter,
|
||||
})
|
||||
}
|
||||
if (
|
||||
this.dateAddedRelativeDate !== null ||
|
||||
this.dateCreatedRelativeDate !== null
|
||||
) {
|
||||
let queryArgs: Array<string> = []
|
||||
let existingRule = filterRules.find(
|
||||
(fr) => fr.rule_type == FILTER_FULLTEXT_QUERY
|
||||
)
|
||||
|
||||
// if had a title / content search and added a relative date we need to carry it over...
|
||||
if (
|
||||
!existingRule &&
|
||||
this._textFilter?.length > 0 &&
|
||||
(this.textFilterTarget == TEXT_FILTER_TARGET_TITLE_CONTENT ||
|
||||
this.textFilterTarget == TEXT_FILTER_TARGET_TITLE)
|
||||
) {
|
||||
existingRule = filterRules.find(
|
||||
(fr) =>
|
||||
fr.rule_type == FILTER_TITLE_CONTENT || fr.rule_type == FILTER_TITLE
|
||||
)
|
||||
existingRule.rule_type = FILTER_FULLTEXT_QUERY
|
||||
}
|
||||
|
||||
let existingRuleArgs = existingRule?.value.split(',')
|
||||
if (this.dateCreatedRelativeDate !== null) {
|
||||
queryArgs.push(
|
||||
`created:[${
|
||||
RELATIVE_DATE_QUERYSTRINGS.find(
|
||||
(qS) => qS.relativeDate == this.dateCreatedRelativeDate
|
||||
).dateQuery
|
||||
}]`
|
||||
)
|
||||
if (existingRule) {
|
||||
queryArgs = existingRuleArgs
|
||||
.filter((arg) => !arg.match(RELATIVE_DATE_QUERY_REGEXP_CREATED))
|
||||
.concat(queryArgs)
|
||||
}
|
||||
}
|
||||
if (this.dateAddedRelativeDate !== null) {
|
||||
queryArgs.push(
|
||||
`added:[${
|
||||
RELATIVE_DATE_QUERYSTRINGS.find(
|
||||
(qS) => qS.relativeDate == this.dateAddedRelativeDate
|
||||
).dateQuery
|
||||
}]`
|
||||
)
|
||||
if (existingRule) {
|
||||
queryArgs = existingRuleArgs
|
||||
.filter((arg) => !arg.match(RELATIVE_DATE_QUERY_REGEXP_ADDED))
|
||||
.concat(queryArgs)
|
||||
}
|
||||
}
|
||||
|
||||
if (existingRule) {
|
||||
existingRule.value = queryArgs.join(',')
|
||||
} else {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_FULLTEXT_QUERY,
|
||||
value: queryArgs.join(','),
|
||||
})
|
||||
}
|
||||
}
|
||||
if (
|
||||
this.dateCreatedRelativeDate == null &&
|
||||
this.dateAddedRelativeDate == null
|
||||
) {
|
||||
const existingRule = filterRules.find(
|
||||
(fr) => fr.rule_type == FILTER_FULLTEXT_QUERY
|
||||
)
|
||||
if (
|
||||
existingRule?.value.match(RELATIVE_DATE_QUERY_REGEXP_CREATED) ||
|
||||
existingRule?.value.match(RELATIVE_DATE_QUERY_REGEXP_ADDED)
|
||||
) {
|
||||
// remove any existing date query
|
||||
existingRule.value = existingRule.value
|
||||
.replace(RELATIVE_DATE_QUERY_REGEXP_CREATED, '')
|
||||
.replace(RELATIVE_DATE_QUERY_REGEXP_ADDED, '')
|
||||
if (existingRule.value.replace(',', '').trim() === '') {
|
||||
// if its empty now, remove it entirely
|
||||
filterRules.splice(filterRules.indexOf(existingRule), 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
return filterRules
|
||||
}
|
||||
|
||||
@@ -569,15 +709,23 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
this.updateRules()
|
||||
}
|
||||
|
||||
textFilterEnter() {
|
||||
const filterString = (
|
||||
this.textFilterInput.nativeElement as HTMLInputElement
|
||||
).value
|
||||
if (filterString.length) {
|
||||
this.updateTextFilter(filterString)
|
||||
textFilterKeyup(event: KeyboardEvent) {
|
||||
if (event.key == 'Enter') {
|
||||
const filterString = (
|
||||
this.textFilterInput.nativeElement as HTMLInputElement
|
||||
).value
|
||||
if (filterString.length) {
|
||||
this.updateTextFilter(filterString)
|
||||
}
|
||||
} else if (event.key == 'Escape') {
|
||||
this.resetTextField()
|
||||
}
|
||||
}
|
||||
|
||||
resetTextField() {
|
||||
this.updateTextFilter('')
|
||||
}
|
||||
|
||||
changeTextFilterTarget(target) {
|
||||
if (
|
||||
this.textFilterTarget == TEXT_FILTER_TARGET_FULLTEXT_MORELIKE &&
|
||||
|
@@ -1,4 +1,4 @@
|
||||
::ng-deep .popover {
|
||||
::ng-deep app-document-list .popover {
|
||||
max-width: 40rem;
|
||||
|
||||
.preview {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<app-page-header title="Settings" i18n-title>
|
||||
|
||||
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()"><ng-container i18n>Start tour</ng-container></button>
|
||||
</app-page-header>
|
||||
|
||||
<!-- <p>items per page, documents per view type</p> -->
|
||||
@@ -89,6 +89,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 col-form-label">
|
||||
<span i18n>Sidebar</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
|
||||
<app-input-check i18n-title title="Use 'slim' sidebar (icons only)" formControlName="slimSidebarEnabled"></app-input-check>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 col-form-label">
|
||||
<span i18n>Dark mode</span>
|
||||
@@ -116,6 +127,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4" id="update-checking" i18n>Update checking</h4>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="offset-md-3 col">
|
||||
<p i18n>
|
||||
Update checking works by pinging the the public <a href="https://api.github.com/repos/paperless-ngx/paperless-ngx/releases/latest" target="_blank" rel="noopener noreferrer">Github API</a> for the latest release to determine whether a new version is available.<br/>
|
||||
Actual updating of the app must still be performed manually.
|
||||
</p>
|
||||
<p i18n>
|
||||
<em>No tracking data is collected by the app in any way.</em>
|
||||
</p>
|
||||
<app-input-check i18n-title title="Enable update checking" formControlName="updateCheckingEnabled" i18n-hint hint="Note that for users of thirdy-party containers e.g. linuxserver.io this notification may be 'ahead' of the current third-party release."></app-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4" i18n>Bulk editing</h4>
|
||||
|
||||
<div class="row mb-3">
|
||||
@@ -194,5 +220,5 @@
|
||||
|
||||
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" [disabled]="!(isDirty$ | async)" i18n>Save</button>
|
||||
<button type="submit" class="btn btn-primary mb-2" [disabled]="!(isDirty$ | async)" i18n>Save</button>
|
||||
</form>
|
||||
|
@@ -4,7 +4,7 @@ import {
|
||||
LOCALE_ID,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
Renderer2,
|
||||
AfterViewInit,
|
||||
} from '@angular/core'
|
||||
import { FormControl, FormGroup } from '@angular/forms'
|
||||
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
|
||||
@@ -16,21 +16,35 @@ import {
|
||||
} from 'src/app/services/settings.service'
|
||||
import { Toast, ToastService } from 'src/app/services/toast.service'
|
||||
import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms'
|
||||
import { Observable, Subscription, BehaviorSubject, first } from 'rxjs'
|
||||
import {
|
||||
Observable,
|
||||
Subscription,
|
||||
BehaviorSubject,
|
||||
first,
|
||||
tap,
|
||||
takeUntil,
|
||||
Subject,
|
||||
} from 'rxjs'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ViewportScroller } from '@angular/common'
|
||||
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings',
|
||||
templateUrl: './settings.component.html',
|
||||
styleUrls: ['./settings.component.scss'],
|
||||
})
|
||||
export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
|
||||
export class SettingsComponent
|
||||
implements OnInit, AfterViewInit, OnDestroy, DirtyComponent
|
||||
{
|
||||
savedViewGroup = new FormGroup({})
|
||||
|
||||
settingsForm = new FormGroup({
|
||||
bulkEditConfirmationDialogs: new FormControl(null),
|
||||
bulkEditApplyOnClose: new FormControl(null),
|
||||
documentListItemPerPage: new FormControl(null),
|
||||
slimSidebarEnabled: new FormControl(null),
|
||||
darkModeUseSystem: new FormControl(null),
|
||||
darkModeEnabled: new FormControl(null),
|
||||
darkModeInvertThumbs: new FormControl(null),
|
||||
@@ -45,6 +59,7 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
|
||||
notificationsConsumerFailed: new FormControl(null),
|
||||
notificationsConsumerSuppressOnDashboard: new FormControl(null),
|
||||
commentsEnabled: new FormControl(null),
|
||||
updateCheckingEnabled: new FormControl(null),
|
||||
})
|
||||
|
||||
savedViews: PaperlessSavedView[]
|
||||
@@ -52,7 +67,9 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
|
||||
store: BehaviorSubject<any>
|
||||
storeSub: Subscription
|
||||
isDirty$: Observable<boolean>
|
||||
isDirty: Boolean = false
|
||||
isDirty: boolean = false
|
||||
unsubscribeNotifier: Subject<any> = new Subject()
|
||||
savePending: boolean = false
|
||||
|
||||
get computedDateLocale(): string {
|
||||
return (
|
||||
@@ -62,105 +79,129 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
|
||||
)
|
||||
}
|
||||
|
||||
get displayLanguageIsDirty(): boolean {
|
||||
return (
|
||||
this.settingsForm.get('displayLanguage').value !=
|
||||
this.store?.getValue()['displayLanguage']
|
||||
)
|
||||
}
|
||||
|
||||
constructor(
|
||||
public savedViewService: SavedViewService,
|
||||
private documentListViewService: DocumentListViewService,
|
||||
private toastService: ToastService,
|
||||
private settings: SettingsService,
|
||||
@Inject(LOCALE_ID) public currentLocale: string
|
||||
) {}
|
||||
@Inject(LOCALE_ID) public currentLocale: string,
|
||||
private viewportScroller: ViewportScroller,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
public readonly tourService: TourService
|
||||
) {
|
||||
this.settings.settingsSaved.subscribe(() => {
|
||||
if (!this.savePending) this.initialize()
|
||||
})
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
if (this.activatedRoute.snapshot.fragment) {
|
||||
this.viewportScroller.scrollToAnchor(
|
||||
this.activatedRoute.snapshot.fragment
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private getCurrentSettings() {
|
||||
return {
|
||||
bulkEditConfirmationDialogs: this.settings.get(
|
||||
SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS
|
||||
),
|
||||
bulkEditApplyOnClose: this.settings.get(
|
||||
SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE
|
||||
),
|
||||
documentListItemPerPage: this.settings.get(
|
||||
SETTINGS_KEYS.DOCUMENT_LIST_SIZE
|
||||
),
|
||||
slimSidebarEnabled: this.settings.get(SETTINGS_KEYS.SLIM_SIDEBAR),
|
||||
darkModeUseSystem: this.settings.get(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM),
|
||||
darkModeEnabled: this.settings.get(SETTINGS_KEYS.DARK_MODE_ENABLED),
|
||||
darkModeInvertThumbs: this.settings.get(
|
||||
SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED
|
||||
),
|
||||
themeColor: this.settings.get(SETTINGS_KEYS.THEME_COLOR),
|
||||
useNativePdfViewer: this.settings.get(
|
||||
SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER
|
||||
),
|
||||
savedViews: {},
|
||||
displayLanguage: this.settings.getLanguage(),
|
||||
dateLocale: this.settings.get(SETTINGS_KEYS.DATE_LOCALE),
|
||||
dateFormat: this.settings.get(SETTINGS_KEYS.DATE_FORMAT),
|
||||
notificationsConsumerNewDocument: this.settings.get(
|
||||
SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT
|
||||
),
|
||||
notificationsConsumerSuccess: this.settings.get(
|
||||
SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS
|
||||
),
|
||||
notificationsConsumerFailed: this.settings.get(
|
||||
SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_FAILED
|
||||
),
|
||||
notificationsConsumerSuppressOnDashboard: this.settings.get(
|
||||
SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD
|
||||
),
|
||||
commentsEnabled: this.settings.get(SETTINGS_KEYS.COMMENTS_ENABLED),
|
||||
updateCheckingEnabled: this.settings.get(
|
||||
SETTINGS_KEYS.UPDATE_CHECKING_ENABLED
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.savedViewService.listAll().subscribe((r) => {
|
||||
this.savedViews = r.results
|
||||
let storeData = {
|
||||
bulkEditConfirmationDialogs: this.settings.get(
|
||||
SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS
|
||||
),
|
||||
bulkEditApplyOnClose: this.settings.get(
|
||||
SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE
|
||||
),
|
||||
documentListItemPerPage: this.settings.get(
|
||||
SETTINGS_KEYS.DOCUMENT_LIST_SIZE
|
||||
),
|
||||
darkModeUseSystem: this.settings.get(
|
||||
SETTINGS_KEYS.DARK_MODE_USE_SYSTEM
|
||||
),
|
||||
darkModeEnabled: this.settings.get(SETTINGS_KEYS.DARK_MODE_ENABLED),
|
||||
darkModeInvertThumbs: this.settings.get(
|
||||
SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED
|
||||
),
|
||||
themeColor: this.settings.get(SETTINGS_KEYS.THEME_COLOR),
|
||||
useNativePdfViewer: this.settings.get(
|
||||
SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER
|
||||
),
|
||||
savedViews: {},
|
||||
displayLanguage: this.settings.getLanguage(),
|
||||
dateLocale: this.settings.get(SETTINGS_KEYS.DATE_LOCALE),
|
||||
dateFormat: this.settings.get(SETTINGS_KEYS.DATE_FORMAT),
|
||||
notificationsConsumerNewDocument: this.settings.get(
|
||||
SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT
|
||||
),
|
||||
notificationsConsumerSuccess: this.settings.get(
|
||||
SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS
|
||||
),
|
||||
notificationsConsumerFailed: this.settings.get(
|
||||
SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_FAILED
|
||||
),
|
||||
notificationsConsumerSuppressOnDashboard: this.settings.get(
|
||||
SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD
|
||||
),
|
||||
commentsEnabled: this.settings.get(SETTINGS_KEYS.COMMENTS_ENABLED),
|
||||
this.initialize()
|
||||
})
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.unsubscribeNotifier.next(true)
|
||||
|
||||
let storeData = this.getCurrentSettings()
|
||||
|
||||
for (let view of this.savedViews) {
|
||||
storeData.savedViews[view.id.toString()] = {
|
||||
id: view.id,
|
||||
name: view.name,
|
||||
show_on_dashboard: view.show_on_dashboard,
|
||||
show_in_sidebar: view.show_in_sidebar,
|
||||
}
|
||||
this.savedViewGroup.addControl(
|
||||
view.id.toString(),
|
||||
new FormGroup({
|
||||
id: new FormControl(null),
|
||||
name: new FormControl(null),
|
||||
show_on_dashboard: new FormControl(null),
|
||||
show_in_sidebar: new FormControl(null),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
for (let view of this.savedViews) {
|
||||
storeData.savedViews[view.id.toString()] = {
|
||||
id: view.id,
|
||||
name: view.name,
|
||||
show_on_dashboard: view.show_on_dashboard,
|
||||
show_in_sidebar: view.show_in_sidebar,
|
||||
}
|
||||
this.savedViewGroup.addControl(
|
||||
view.id.toString(),
|
||||
new FormGroup({
|
||||
id: new FormControl(null),
|
||||
name: new FormControl(null),
|
||||
show_on_dashboard: new FormControl(null),
|
||||
show_in_sidebar: new FormControl(null),
|
||||
})
|
||||
)
|
||||
}
|
||||
this.store = new BehaviorSubject(storeData)
|
||||
|
||||
this.store = new BehaviorSubject(storeData)
|
||||
this.storeSub = this.store.asObservable().subscribe((state) => {
|
||||
this.settingsForm.patchValue(state, { emitEvent: false })
|
||||
})
|
||||
|
||||
this.storeSub = this.store.asObservable().subscribe((state) => {
|
||||
this.settingsForm.patchValue(state, { emitEvent: false })
|
||||
})
|
||||
// Initialize dirtyCheck
|
||||
this.isDirty$ = dirtyCheck(this.settingsForm, this.store.asObservable())
|
||||
|
||||
// Initialize dirtyCheck
|
||||
this.isDirty$ = dirtyCheck(this.settingsForm, this.store.asObservable())
|
||||
|
||||
// Record dirty in case we need to 'undo' appearance settings if not saved on close
|
||||
this.isDirty$.subscribe((dirty) => {
|
||||
// Record dirty in case we need to 'undo' appearance settings if not saved on close
|
||||
this.isDirty$
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((dirty) => {
|
||||
this.isDirty = dirty
|
||||
})
|
||||
|
||||
// "Live" visual changes prior to save
|
||||
this.settingsForm.valueChanges.subscribe(() => {
|
||||
// "Live" visual changes prior to save
|
||||
this.settingsForm.valueChanges
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
this.settings.updateAppearanceSettings(
|
||||
this.settingsForm.get('darkModeUseSystem').value,
|
||||
this.settingsForm.get('darkModeEnabled').value,
|
||||
this.settingsForm.get('themeColor').value
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -179,7 +220,14 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
|
||||
}
|
||||
|
||||
private saveLocalSettings() {
|
||||
const reloadRequired = this.displayLanguageIsDirty // just this one, for now
|
||||
this.savePending = true
|
||||
const reloadRequired =
|
||||
this.settingsForm.value.displayLanguage !=
|
||||
this.store?.getValue()['displayLanguage'] || // displayLanguage is dirty
|
||||
(this.settingsForm.value.updateCheckingEnabled !=
|
||||
this.store?.getValue()['updateCheckingEnabled'] &&
|
||||
this.settingsForm.value.updateCheckingEnabled) // update checking was turned on
|
||||
|
||||
this.settings.set(
|
||||
SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE,
|
||||
this.settingsForm.value.bulkEditApplyOnClose
|
||||
@@ -192,6 +240,10 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
|
||||
SETTINGS_KEYS.DOCUMENT_LIST_SIZE,
|
||||
this.settingsForm.value.documentListItemPerPage
|
||||
)
|
||||
this.settings.set(
|
||||
SETTINGS_KEYS.SLIM_SIDEBAR,
|
||||
this.settingsForm.value.slimSidebarEnabled
|
||||
)
|
||||
this.settings.set(
|
||||
SETTINGS_KEYS.DARK_MODE_USE_SYSTEM,
|
||||
this.settingsForm.value.darkModeUseSystem
|
||||
@@ -240,10 +292,15 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
|
||||
SETTINGS_KEYS.COMMENTS_ENABLED,
|
||||
this.settingsForm.value.commentsEnabled
|
||||
)
|
||||
this.settings.set(
|
||||
SETTINGS_KEYS.UPDATE_CHECKING_ENABLED,
|
||||
this.settingsForm.value.updateCheckingEnabled
|
||||
)
|
||||
this.settings.setLanguage(this.settingsForm.value.displayLanguage)
|
||||
this.settings
|
||||
.storeSettings()
|
||||
.pipe(first())
|
||||
.pipe(tap(() => (this.savePending = false)))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.store.next(this.settingsForm.value)
|
||||
|
@@ -53,8 +53,8 @@
|
||||
<label class="form-check-label" for="task{{task.id}}"></label>
|
||||
</div>
|
||||
</th>
|
||||
<td class="overflow-auto">{{ task.name }}</td>
|
||||
<td class="d-none d-lg-table-cell">{{ task.created | customDate:'short' }}</td>
|
||||
<td class="overflow-auto">{{ task.task_file_name }}</td>
|
||||
<td class="d-none d-lg-table-cell">{{ task.date_created | customDate:'short' }}</td>
|
||||
<td class="d-none d-lg-table-cell" *ngIf="activeTab != 'started' && activeTab != 'queued'">
|
||||
<div *ngIf="task.result.length > 50" class="result" (click)="expandTask(task); $event.stopPropagation();"
|
||||
[ngbPopover]="resultPopover" popoverClass="shadow small mobile" triggers="mouseenter:mouseleave" container="body">
|
||||
@@ -74,11 +74,18 @@
|
||||
</button>
|
||||
</td>
|
||||
<td scope="row">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="dismissTask(task); $event.stopPropagation();">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#check"/>
|
||||
</svg> <ng-container i18n>Dismiss</ng-container>
|
||||
</button>
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="dismissTask(task); $event.stopPropagation();">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#check"/>
|
||||
</svg> <ng-container i18n>Dismiss</ng-container>
|
||||
</button>
|
||||
<button *ngIf="task.related_document" class="btn btn-sm btn-outline-primary" (click)="dismissAndGo(task); $event.stopPropagation();">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
|
||||
</svg> <ng-container i18n>Open Document</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { takeUntil, Subject, first } from 'rxjs'
|
||||
import { Subject, first } from 'rxjs'
|
||||
import { PaperlessTask } from 'src/app/data/paperless-task'
|
||||
import { TasksService } from 'src/app/services/tasks.service'
|
||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||
@@ -24,7 +25,8 @@ export class TasksComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor(
|
||||
public tasksService: TasksService,
|
||||
private modalService: NgbModal
|
||||
private modalService: NgbModal,
|
||||
private readonly router: Router
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -64,6 +66,11 @@ export class TasksComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
dismissAndGo(task: PaperlessTask) {
|
||||
this.dismissTask(task)
|
||||
this.router.navigate(['documents', task.related_document])
|
||||
}
|
||||
|
||||
expandTask(task: PaperlessTask) {
|
||||
this.expandedTask = this.expandedTask == task.id ? undefined : task.id
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ export const MATCH_LITERAL = 3
|
||||
export const MATCH_REGEX = 4
|
||||
export const MATCH_FUZZY = 5
|
||||
export const MATCH_AUTO = 6
|
||||
export const DEFAULT_MATCHING_ALGORITHM = MATCH_AUTO
|
||||
|
||||
export const MATCHING_ALGORITHMS = [
|
||||
{
|
||||
|
@@ -29,8 +29,6 @@ export interface PaperlessDocument extends ObjectWithId {
|
||||
|
||||
content?: string
|
||||
|
||||
file_type?: string
|
||||
|
||||
tags$?: Observable<PaperlessTag[]>
|
||||
|
||||
tags?: number[]
|
||||
@@ -47,7 +45,7 @@ export interface PaperlessDocument extends ObjectWithId {
|
||||
|
||||
added?: Date
|
||||
|
||||
file_name?: string
|
||||
original_file_name?: string
|
||||
|
||||
download_url?: string
|
||||
|
||||
|
@@ -6,11 +6,10 @@ export enum PaperlessTaskType {
|
||||
}
|
||||
|
||||
export enum PaperlessTaskStatus {
|
||||
Queued = 'queued',
|
||||
Started = 'started',
|
||||
Complete = 'complete',
|
||||
Failed = 'failed',
|
||||
Unknown = 'unknown',
|
||||
Pending = 'PENDING',
|
||||
Started = 'STARTED',
|
||||
Complete = 'SUCCESS',
|
||||
Failed = 'FAILURE',
|
||||
}
|
||||
|
||||
export interface PaperlessTask extends ObjectWithId {
|
||||
@@ -22,11 +21,13 @@ export interface PaperlessTask extends ObjectWithId {
|
||||
|
||||
task_id: string
|
||||
|
||||
name: string
|
||||
task_file_name: string
|
||||
|
||||
created: Date
|
||||
date_created: Date
|
||||
|
||||
started?: Date
|
||||
done?: Date
|
||||
|
||||
result: string
|
||||
|
||||
related_document?: number
|
||||
}
|
||||
|
@@ -37,6 +37,10 @@ export const SETTINGS_KEYS = {
|
||||
NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD:
|
||||
'general-settings:notifications:consumer-suppress-on-dashboard',
|
||||
COMMENTS_ENABLED: 'general-settings:comments-enabled',
|
||||
SLIM_SIDEBAR: 'general-settings:slim-sidebar',
|
||||
UPDATE_CHECKING_ENABLED: 'general-settings:update-checking:enabled',
|
||||
UPDATE_CHECKING_BACKEND_SETTING:
|
||||
'general-settings:update-checking:backend-setting',
|
||||
}
|
||||
|
||||
export const SETTINGS: PaperlessUiSetting[] = [
|
||||
@@ -55,6 +59,11 @@ export const SETTINGS: PaperlessUiSetting[] = [
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
key: SETTINGS_KEYS.SLIM_SIDEBAR,
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
key: SETTINGS_KEYS.DOCUMENT_LIST_SIZE,
|
||||
type: 'number',
|
||||
@@ -120,4 +129,14 @@ export const SETTINGS: PaperlessUiSetting[] = [
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
key: SETTINGS_KEYS.UPDATE_CHECKING_ENABLED,
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
key: SETTINGS_KEYS.UPDATE_CHECKING_BACKEND_SETTING,
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
]
|
||||
|
51
src-ui/src/app/guards/dirty-saved-view.guard.ts
Normal file
51
src-ui/src/app/guards/dirty-saved-view.guard.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { CanDeactivate } from '@angular/router'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { first, Observable, Subject } from 'rxjs'
|
||||
import { DocumentListComponent } from '../components/document-list/document-list.component'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ConfirmDialogComponent } from '../components/common/confirm-dialog/confirm-dialog.component'
|
||||
|
||||
@Injectable()
|
||||
export class DirtySavedViewGuard
|
||||
implements CanDeactivate<DocumentListComponent>
|
||||
{
|
||||
constructor(private modalService: NgbModal) {}
|
||||
|
||||
canDeactivate(
|
||||
component: DocumentListComponent
|
||||
): boolean | Observable<boolean> {
|
||||
return component.savedViewIsModified ? this.warn(component) : true
|
||||
}
|
||||
|
||||
warn(component: DocumentListComponent) {
|
||||
let modal = this.modalService.open(ConfirmDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.title = $localize`Unsaved Changes`
|
||||
modal.componentInstance.messageBold =
|
||||
$localize`You have unsaved changes to the saved view` +
|
||||
' "' +
|
||||
component.getTitle()
|
||||
;('".')
|
||||
modal.componentInstance.message = $localize`Are you sure you want to close this saved view?`
|
||||
modal.componentInstance.btnClass = 'btn-secondary'
|
||||
modal.componentInstance.btnCaption = $localize`Close`
|
||||
modal.componentInstance.alternativeBtnClass = 'btn-primary'
|
||||
modal.componentInstance.alternativeBtnCaption = $localize`Save and close`
|
||||
modal.componentInstance.alternativeClicked.pipe(first()).subscribe(() => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
component.saveViewConfig()
|
||||
modal.close()
|
||||
})
|
||||
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
modal.close()
|
||||
})
|
||||
|
||||
const subject = new Subject<boolean>()
|
||||
modal.componentInstance.confirmSubject = subject
|
||||
modal.componentInstance.alternativeSubject = subject
|
||||
|
||||
return subject
|
||||
}
|
||||
}
|
@@ -171,15 +171,15 @@ export class DocumentListViewService {
|
||||
this.reduceSelectionToFilter()
|
||||
|
||||
if (!this.router.routerState.snapshot.url.includes('/view/')) {
|
||||
this.router.navigate([], {
|
||||
queryParams: { view: view.id },
|
||||
})
|
||||
this.router.navigate(['view', view.id])
|
||||
}
|
||||
}
|
||||
|
||||
loadFromQueryParams(queryParams: ParamMap) {
|
||||
const paramsEmpty: boolean = queryParams.keys.length == 0
|
||||
let newState: ListViewState = this.listViewStates.get(null)
|
||||
let newState: ListViewState = this.listViewStates.get(
|
||||
this._activeSavedViewId
|
||||
)
|
||||
if (!paramsEmpty) newState = paramsToViewState(queryParams)
|
||||
if (newState == undefined) newState = this.defaultListViewState() // if nothing in local storage
|
||||
|
||||
@@ -276,7 +276,6 @@ export class DocumentListViewService {
|
||||
) {
|
||||
this.activeListViewState.sortField = 'created'
|
||||
}
|
||||
this._activeSavedViewId = null
|
||||
this.activeListViewState.filterRules = filterRules
|
||||
this.reload()
|
||||
this.reduceSelectionToFilter()
|
||||
@@ -288,7 +287,6 @@ export class DocumentListViewService {
|
||||
}
|
||||
|
||||
set sortField(field: string) {
|
||||
this._activeSavedViewId = null
|
||||
this.activeListViewState.sortField = field
|
||||
this.reload()
|
||||
this.saveDocumentListView()
|
||||
@@ -299,7 +297,6 @@ export class DocumentListViewService {
|
||||
}
|
||||
|
||||
set sortReverse(reverse: boolean) {
|
||||
this._activeSavedViewId = null
|
||||
this.activeListViewState.sortReverse = reverse
|
||||
this.reload()
|
||||
this.saveDocumentListView()
|
||||
|
@@ -6,7 +6,6 @@ import { environment } from 'src/environments/environment'
|
||||
export interface AppRemoteVersion {
|
||||
version: string
|
||||
update_available: boolean
|
||||
feature_is_set: boolean
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import {
|
||||
EventEmitter,
|
||||
Inject,
|
||||
Injectable,
|
||||
LOCALE_ID,
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
SETTINGS,
|
||||
SETTINGS_KEYS,
|
||||
} from '../data/paperless-uisettings'
|
||||
import { SavedViewService } from './rest/saved-view.service'
|
||||
import { ToastService } from './toast.service'
|
||||
|
||||
export interface LanguageOption {
|
||||
@@ -46,6 +48,8 @@ export class SettingsService {
|
||||
|
||||
public displayName: string
|
||||
|
||||
public settingsSaved: EventEmitter<any> = new EventEmitter()
|
||||
|
||||
constructor(
|
||||
rendererFactory: RendererFactory2,
|
||||
@Inject(DOCUMENT) private document,
|
||||
@@ -53,7 +57,8 @@ export class SettingsService {
|
||||
private meta: Meta,
|
||||
@Inject(LOCALE_ID) private localeId: string,
|
||||
protected http: HttpClient,
|
||||
private toastService: ToastService
|
||||
private toastService: ToastService,
|
||||
private savedViewService: SavedViewService
|
||||
) {
|
||||
this.renderer = rendererFactory.createRenderer(null, null)
|
||||
}
|
||||
@@ -313,13 +318,7 @@ export class SettingsService {
|
||||
)
|
||||
}
|
||||
|
||||
get(key: string): any {
|
||||
let setting = SETTINGS.find((s) => s.key == key)
|
||||
|
||||
if (!setting) {
|
||||
return null
|
||||
}
|
||||
|
||||
private getSettingRawValue(key: string): any {
|
||||
let value = null
|
||||
// parse key:key:key into nested object
|
||||
const keys = key.replace('general-settings:', '').split(':')
|
||||
@@ -330,6 +329,17 @@ export class SettingsService {
|
||||
if (index == keys.length - 1) value = settingObj[keyPart]
|
||||
else settingObj = settingObj[keyPart]
|
||||
})
|
||||
return value
|
||||
}
|
||||
|
||||
get(key: string): any {
|
||||
let setting = SETTINGS.find((s) => s.key == key)
|
||||
|
||||
if (!setting) {
|
||||
return null
|
||||
}
|
||||
|
||||
let value = this.getSettingRawValue(key)
|
||||
|
||||
if (value != null) {
|
||||
switch (setting.type) {
|
||||
@@ -359,8 +369,19 @@ export class SettingsService {
|
||||
})
|
||||
}
|
||||
|
||||
private settingIsSet(key: string): boolean {
|
||||
let value = this.getSettingRawValue(key)
|
||||
return value != null
|
||||
}
|
||||
|
||||
storeSettings(): Observable<any> {
|
||||
return this.http.post(this.baseUrl, { settings: this.settings })
|
||||
return this.http.post(this.baseUrl, { settings: this.settings }).pipe(
|
||||
tap((results) => {
|
||||
if (results.success) {
|
||||
this.settingsSaved.emit()
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
maybeMigrateSettings() {
|
||||
@@ -400,5 +421,38 @@ export class SettingsService {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
!this.settingIsSet(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED) &&
|
||||
this.get(SETTINGS_KEYS.UPDATE_CHECKING_BACKEND_SETTING) != 'default'
|
||||
) {
|
||||
this.set(
|
||||
SETTINGS_KEYS.UPDATE_CHECKING_ENABLED,
|
||||
this.get(SETTINGS_KEYS.UPDATE_CHECKING_BACKEND_SETTING).toString() ===
|
||||
'true'
|
||||
)
|
||||
|
||||
this.storeSettings()
|
||||
.pipe(first())
|
||||
.subscribe({
|
||||
error: (e) => {
|
||||
this.toastService.showError(
|
||||
'Error migrating update checking setting'
|
||||
)
|
||||
console.log(e)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
get updateCheckingIsSet(): boolean {
|
||||
return this.settingIsSet(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED)
|
||||
}
|
||||
|
||||
offerTour(): boolean {
|
||||
return (
|
||||
!this.savedViewService.loading &&
|
||||
this.savedViewService.dashboardViews.length == 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { first, map } from 'rxjs/operators'
|
||||
import { first } from 'rxjs/operators'
|
||||
import {
|
||||
PaperlessTask,
|
||||
PaperlessTaskStatus,
|
||||
@@ -27,7 +27,7 @@ export class TasksService {
|
||||
}
|
||||
|
||||
public get queuedFileTasks(): PaperlessTask[] {
|
||||
return this.fileTasks.filter((t) => t.status == PaperlessTaskStatus.Queued)
|
||||
return this.fileTasks.filter((t) => t.status == PaperlessTaskStatus.Pending)
|
||||
}
|
||||
|
||||
public get startedFileTasks(): PaperlessTask[] {
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 8.1 KiB |
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en" datatype="plaintext" original="ng2.template" target-language="ar">
|
||||
<file source-language="en" datatype="plaintext" original="ng2.template" target-language="ar-AR">
|
||||
<body>
|
||||
<trans-unit id="ngb.alert.close" datatype="html">
|
||||
<source>Close</source>
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
||||
// bs options
|
||||
$enable-negative-margins: true;
|
||||
|
||||
@import "theme";
|
||||
@import "node_modules/bootstrap/scss/bootstrap";
|
||||
@import "~@ng-select/ng-select/themes/default.theme.css";
|
||||
@import "theme";
|
||||
@import "print";
|
||||
|
||||
// Paperless-ngx styles
|
||||
@@ -73,8 +73,8 @@ svg.logo {
|
||||
border-color: var(--bs-primary);
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: var(--pngx-primary-darken-5);
|
||||
border-color: var(--pngx-primary-darken-5);
|
||||
background-color: var(--pngx-primary-darken-5) !important;
|
||||
border-color: var(--pngx-primary-darken-5) !important;
|
||||
}
|
||||
|
||||
&:disabled, &.disabled {
|
||||
@@ -84,6 +84,15 @@ svg.logo {
|
||||
}
|
||||
}
|
||||
|
||||
.btn-dark {
|
||||
--bs-btn-color: var(--bs-gray-600);
|
||||
--bs-btn-bg: var(--bs-gray-200);
|
||||
--bs-btn-border-color: var(--bs-gray-200);
|
||||
--bs-btn-hover-bg: var(--bs-gray-400);
|
||||
--bs-btn-hover-border-color: var(--bs-gray-500);
|
||||
--bs-btn-active-bg: var(--bs-gray-200);
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: var(--bs-primary) !important;
|
||||
}
|
||||
@@ -155,10 +164,16 @@ a.navbar-brand:focus-visible {
|
||||
}
|
||||
}
|
||||
|
||||
a, a:hover, .btn-link, .btn-link:hover {
|
||||
a, a:hover,
|
||||
.btn-link {
|
||||
color: var(--bs-primary);
|
||||
}
|
||||
|
||||
.btn-link:hover,
|
||||
.btn-link:active {
|
||||
color: var(--pngx-primary-darken-15) !important;
|
||||
}
|
||||
|
||||
.form-control-dark {
|
||||
color: #fff;
|
||||
background-color: rgba(255, 255, 255, .1);
|
||||
@@ -357,6 +372,10 @@ textarea,
|
||||
&:hover, &:focus {
|
||||
background-color: var(--bs-body-bg);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
@@ -378,6 +397,10 @@ textarea,
|
||||
background-color: var(--bs-primary);
|
||||
color: var(--pngx-primary-text-contrast);
|
||||
}
|
||||
|
||||
&.disabled, &:disabled {
|
||||
opacity: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,6 +428,11 @@ textarea,
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
.sidebaricon-sm {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
table.table {
|
||||
color: var(--bs-body-color);
|
||||
|
||||
@@ -465,6 +493,12 @@ table.table {
|
||||
user-select: none !important;
|
||||
}
|
||||
|
||||
.alert-primary {
|
||||
--bs-alert-color: var(--bs-primary);
|
||||
--bs-alert-bg: var(--pngx-primary-faded);
|
||||
--bs-alert-border-color: var(--bs-primary);
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
color: var(--bs-body-color);
|
||||
background-color: var(--bs-danger);
|
||||
@@ -482,14 +516,33 @@ table.table {
|
||||
}
|
||||
|
||||
.popover {
|
||||
background-color: var(--pngx-bg-alt);
|
||||
.popover-header {
|
||||
background-color: var(--pngx-bg-alt);
|
||||
}
|
||||
.popover-header,
|
||||
.popover-body {
|
||||
background-color: var(--pngx-bg-alt);
|
||||
border-color: var(--bs-border-color);
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
}
|
||||
|
||||
// Tour
|
||||
.tour-active .popover {
|
||||
min-width: 360px;
|
||||
margin: 0 .25rem .25rem !important;
|
||||
}
|
||||
|
||||
body.tour-active .row.sticky-top,
|
||||
body.tour-active .sidebar {
|
||||
z-index: inherit !important;
|
||||
}
|
||||
|
||||
.nav-item.touranchor--is-active a {
|
||||
font-weight: bold !important;
|
||||
color: var(--bs-primary);
|
||||
}
|
||||
|
||||
// fix popover carat colors
|
||||
.bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="left"] {
|
||||
border-left-color: var(--pngx-bg-alt);
|
||||
@@ -526,6 +579,25 @@ a.badge {
|
||||
border-color: var(--bs-primary);
|
||||
}
|
||||
|
||||
.btn-group-xs {
|
||||
> .btn {
|
||||
padding: 0.2rem 0.25rem;
|
||||
font-size: 0.675rem;
|
||||
line-height: 1.2;
|
||||
border-radius: 0.15rem;
|
||||
}
|
||||
|
||||
> .btn:not(:first-child):not(:nth-child(2)) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
> .btn:not(:last-child) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
color: var(--pngx-body-color-accent)
|
||||
}
|
||||
|
@@ -10,11 +10,12 @@ body {
|
||||
|
||||
--bs-primary: hsl(var(--pngx-primary), var(--pngx-primary-lightness));
|
||||
--bs-border-color: var(--bs-gray-400);
|
||||
--pngx-primary-faded: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) + 72%));
|
||||
--pngx-primary-faded: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) + 76%));
|
||||
--pngx-primary-lighten-30: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) + 30%));
|
||||
--pngx-primary-darken-5: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 5%));
|
||||
--pngx-primary-darken-15: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 15%));
|
||||
--pngx-primary-darken-18: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 18%));
|
||||
--pngx-primary-darken-27: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 27%));
|
||||
--pngx-bg-alt: #fff;
|
||||
--pngx-bg-darker: var(--bs-gray-100);
|
||||
--pngx-focus-alpha: 0.3;
|
||||
@@ -79,6 +80,15 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,<svg xmlns='htt
|
||||
}
|
||||
}
|
||||
|
||||
.btn-dark {
|
||||
--bs-btn-color: $text-color-dark-bg;
|
||||
--bs-btn-bg: var(--pngx-bg-alt);
|
||||
--bs-btn-border-color: var(--pngx-bg-alt);
|
||||
--bs-btn-hover-bg: var(--bs-light);
|
||||
--bs-btn-hover-border-color: var(--pngx-bg-darker);
|
||||
--bs-btn-active-bg: var(--pngx-bg-alt);
|
||||
}
|
||||
|
||||
.btn-outline-primary {
|
||||
&:hover, &:focus, &.active, &:active {
|
||||
color: var(--bs-light) !important;
|
||||
@@ -168,6 +178,12 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,<svg xmlns='htt
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
.alert-primary {
|
||||
--bs-alert-color: var(--pngx-primary-text-contrast);
|
||||
--bs-alert-bg: var(--pngx-primary-darken-27);
|
||||
--bs-alert-border-color: var(--pngx-bg-darker);
|
||||
}
|
||||
|
||||
.table-striped > tbody > tr:nth-of-type(odd) > * {
|
||||
color: var(--pngx-body-color-accent);
|
||||
}
|
||||
|
Reference in New Issue
Block a user