mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	added paperless ui
This commit is contained in:
		
							
								
								
									
										42
									
								
								src-ui/src/app/app-routing.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src-ui/src/app/app-routing.module.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { Routes, RouterModule } from '@angular/router'; | ||||
| import { AppFrameComponent } from './components/app-frame/app-frame.component'; | ||||
| import { DashboardComponent } from './components/dashboard/dashboard.component'; | ||||
| import { DocumentDetailComponent } from './components/document-detail/document-detail.component'; | ||||
| import { DocumentListComponent } from './components/document-list/document-list.component'; | ||||
| import { LoginComponent } from './components/login/login.component'; | ||||
| import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'; | ||||
| import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'; | ||||
| import { LogsComponent } from './components/manage/logs/logs.component'; | ||||
| import { SettingsComponent } from './components/manage/settings/settings.component'; | ||||
| import { TagListComponent } from './components/manage/tag-list/tag-list.component'; | ||||
| import { NotFoundComponent } from './components/not-found/not-found.component'; | ||||
| import { SearchComponent } from './components/search/search.component'; | ||||
| import { AuthGuardService } from './services/auth-guard.service'; | ||||
|  | ||||
| const routes: Routes = [ | ||||
|   {path: '', redirectTo: 'dashboard', pathMatch: 'full'}, | ||||
|   {path: '', component: AppFrameComponent, children: [ | ||||
|     {path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuardService] }, | ||||
|     {path: 'documents', component: DocumentListComponent, canActivate: [AuthGuardService] }, | ||||
|     {path: 'view/:name', component: DocumentListComponent, canActivate: [AuthGuardService] }, | ||||
|     {path: 'search', component: SearchComponent, canActivate: [AuthGuardService] }, | ||||
|     {path: 'documents/:id', component: DocumentDetailComponent, canActivate: [AuthGuardService] }, | ||||
|    | ||||
|     {path: 'tags', component: TagListComponent, canActivate: [AuthGuardService] }, | ||||
|     {path: 'documenttypes', component: DocumentTypeListComponent, canActivate: [AuthGuardService] }, | ||||
|     {path: 'correspondents', component: CorrespondentListComponent, canActivate: [AuthGuardService] }, | ||||
|     {path: 'logs', component: LogsComponent, canActivate: [AuthGuardService] }, | ||||
|     {path: 'settings', component: SettingsComponent, canActivate: [AuthGuardService] }, | ||||
|   ]},  | ||||
|  | ||||
|   {path: 'login', component: LoginComponent }, | ||||
|   {path: '404', component: NotFoundComponent}, | ||||
|   {path: '**', redirectTo: '/404', pathMatch: 'full'} | ||||
| ]; | ||||
|  | ||||
| @NgModule({ | ||||
|   imports: [RouterModule.forRoot(routes)], | ||||
|   exports: [RouterModule] | ||||
| }) | ||||
| export class AppRoutingModule { } | ||||
							
								
								
									
										0
									
								
								src-ui/src/app/app.component.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src-ui/src/app/app.component.css
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										3
									
								
								src-ui/src/app/app.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src-ui/src/app/app.component.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| <app-toasts></app-toasts> | ||||
|  | ||||
| <router-outlet></router-outlet> | ||||
							
								
								
									
										35
									
								
								src-ui/src/app/app.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src-ui/src/app/app.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| import { TestBed } from '@angular/core/testing'; | ||||
| import { RouterTestingModule } from '@angular/router/testing'; | ||||
| import { AppComponent } from './app.component'; | ||||
|  | ||||
| describe('AppComponent', () => { | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       imports: [ | ||||
|         RouterTestingModule | ||||
|       ], | ||||
|       declarations: [ | ||||
|         AppComponent | ||||
|       ], | ||||
|     }).compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   it('should create the app', () => { | ||||
|     const fixture = TestBed.createComponent(AppComponent); | ||||
|     const app = fixture.componentInstance; | ||||
|     expect(app).toBeTruthy(); | ||||
|   }); | ||||
|  | ||||
|   it(`should have as title 'paperless-ui'`, () => { | ||||
|     const fixture = TestBed.createComponent(AppComponent); | ||||
|     const app = fixture.componentInstance; | ||||
|     expect(app.title).toEqual('paperless-ui'); | ||||
|   }); | ||||
|  | ||||
|   it('should render title', () => { | ||||
|     const fixture = TestBed.createComponent(AppComponent); | ||||
|     fixture.detectChanges(); | ||||
|     const compiled = fixture.nativeElement; | ||||
|     expect(compiled.querySelector('.content span').textContent).toContain('paperless-ui app is running!'); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										14
									
								
								src-ui/src/app/app.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src-ui/src/app/app.component.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| import { Component } from '@angular/core'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-root', | ||||
|   templateUrl: './app.component.html', | ||||
|   styleUrls: ['./app.component.css'] | ||||
| }) | ||||
| export class AppComponent { | ||||
|    | ||||
|   constructor () { | ||||
|   } | ||||
|  | ||||
|  | ||||
| } | ||||
							
								
								
									
										82
									
								
								src-ui/src/app/app.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src-ui/src/app/app.module.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| import { BrowserModule } from '@angular/platform-browser'; | ||||
| import { NgModule } from '@angular/core'; | ||||
|  | ||||
| import { AppRoutingModule } from './app-routing.module'; | ||||
| import { AppComponent } from './app.component'; | ||||
| import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; | ||||
| import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; | ||||
| import { DocumentListComponent } from './components/document-list/document-list.component'; | ||||
| import { DocumentDetailComponent } from './components/document-detail/document-detail.component'; | ||||
| import { DashboardComponent } from './components/dashboard/dashboard.component'; | ||||
| import { TagListComponent } from './components/manage/tag-list/tag-list.component'; | ||||
| import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'; | ||||
| import { LogsComponent } from './components/manage/logs/logs.component'; | ||||
| import { SettingsComponent } from './components/manage/settings/settings.component'; | ||||
| import { LoginComponent } from './components/login/login.component'; | ||||
| import { FormsModule, ReactiveFormsModule } from '@angular/forms'; | ||||
| import { DatePipe } from '@angular/common'; | ||||
| import { SafePipe } from './pipes/safe.pipe'; | ||||
| import { NotFoundComponent } from './components/not-found/not-found.component'; | ||||
| import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'; | ||||
| import { DeleteDialogComponent } from './components/common/delete-dialog/delete-dialog.component'; | ||||
| import { CorrespondentEditDialogComponent } from './components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component'; | ||||
| import { TagEditDialogComponent } from './components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component'; | ||||
| import { DocumentTypeEditDialogComponent } from './components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component'; | ||||
| import { TagComponent } from './components/common/tag/tag.component'; | ||||
| import { SearchComponent } from './components/search/search.component'; | ||||
| import { ResultHightlightComponent } from './components/search/result-hightlight/result-hightlight.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'; | ||||
| import { FilterEditorComponent } from './components/filter-editor/filter-editor.component'; | ||||
| import { AuthInterceptor } from './services/auth.interceptor'; | ||||
| import { DocumentCardLargeComponent } from './components/document-list/document-card-large/document-card-large.component'; | ||||
| import { DocumentCardSmallComponent } from './components/document-list/document-card-small/document-card-small.component'; | ||||
|  | ||||
| @NgModule({ | ||||
|   declarations: [ | ||||
|     AppComponent, | ||||
|     DocumentListComponent, | ||||
|     DocumentDetailComponent, | ||||
|     DashboardComponent, | ||||
|     TagListComponent, | ||||
|     CorrespondentListComponent, | ||||
|     DocumentTypeListComponent, | ||||
|     LogsComponent, | ||||
|     SettingsComponent, | ||||
|     LoginComponent, | ||||
|     SafePipe, | ||||
|     NotFoundComponent, | ||||
|     CorrespondentEditDialogComponent, | ||||
|     DeleteDialogComponent, | ||||
|     TagEditDialogComponent, | ||||
|     DocumentTypeEditDialogComponent, | ||||
|     TagComponent, | ||||
|     SearchComponent, | ||||
|     ResultHightlightComponent, | ||||
|     PageHeaderComponent, | ||||
|     AppFrameComponent, | ||||
|     ToastsComponent, | ||||
|     FilterEditorComponent, | ||||
|     DocumentCardLargeComponent, | ||||
|     DocumentCardSmallComponent | ||||
|   ], | ||||
|   imports: [ | ||||
|     BrowserModule, | ||||
|     AppRoutingModule, | ||||
|     NgbModule, | ||||
|     HttpClientModule, | ||||
|     FormsModule, | ||||
|     ReactiveFormsModule | ||||
|   ], | ||||
|   providers: [ | ||||
|     DatePipe, | ||||
|     { | ||||
|       provide: HTTP_INTERCEPTORS, | ||||
|       useClass: AuthInterceptor, | ||||
|       multi: true | ||||
|     } | ||||
|   ], | ||||
|   bootstrap: [AppComponent] | ||||
| }) | ||||
| export class AppModule { } | ||||
							
								
								
									
										84
									
								
								src-ui/src/app/components/app-frame/app-frame.component.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								src-ui/src/app/components/app-frame/app-frame.component.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | ||||
|  | ||||
|   /* | ||||
|  * Sidebar | ||||
|  */ | ||||
|  | ||||
|  .sidebar { | ||||
|   position: fixed; | ||||
|   top: 0; | ||||
|   bottom: 0; | ||||
|   left: 0; | ||||
|   z-index: 100; /* Behind the navbar */ | ||||
|   padding: 48px 0 0; /* Height of navbar */ | ||||
|   box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1); | ||||
| } | ||||
|  | ||||
| @media (max-width: 767.98px) { | ||||
|   .sidebar { | ||||
|     top: 5rem; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .sidebar-sticky { | ||||
|   position: relative; | ||||
|   top: 0; | ||||
|   height: calc(100vh - 48px); | ||||
|   padding-top: .5rem; | ||||
|   overflow-x: hidden; | ||||
|   overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ | ||||
| } | ||||
|  | ||||
| @supports ((position: -webkit-sticky) or (position: sticky)) { | ||||
|   .sidebar-sticky { | ||||
|     position: -webkit-sticky; | ||||
|     position: sticky; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .sidebar .nav-link { | ||||
|   font-weight: 500; | ||||
|   color: #333; | ||||
| } | ||||
|  | ||||
| .sidebar .nav-link .sidebaricon { | ||||
|   margin-right: 4px; | ||||
|   color: #999; | ||||
| } | ||||
|  | ||||
| .sidebar .nav-link.active { | ||||
|   color: #007bff; | ||||
| } | ||||
|  | ||||
| .sidebar .nav-link:hover .sidebaricon, | ||||
| .sidebar .nav-link.active .sidebaricon { | ||||
|   color: inherit; | ||||
| } | ||||
|  | ||||
| .sidebar-heading { | ||||
|   font-size: .75rem; | ||||
|   text-transform: uppercase; | ||||
| } | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Navbar | ||||
|  */ | ||||
|  | ||||
|  .navbar-brand { | ||||
|   padding-top: .75rem; | ||||
|   padding-bottom: .75rem; | ||||
|   font-size: 1rem; | ||||
|   background-color: rgba(0, 0, 0, .25); | ||||
|   box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25); | ||||
| } | ||||
|  | ||||
| .navbar .navbar-toggler { | ||||
|   top: .25rem; | ||||
|   right: 1rem; | ||||
| } | ||||
|  | ||||
| .navbar .form-control { | ||||
|   padding: .75rem 1rem; | ||||
|   border-width: 0; | ||||
|   border-radius: 0; | ||||
| } | ||||
							
								
								
									
										112
									
								
								src-ui/src/app/components/app-frame/app-frame.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								src-ui/src/app/components/app-frame/app-frame.component.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | ||||
| <nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow"> | ||||
|   <span class="navbar-brand col-md-3 col-lg-2 mr-0 px-3" href="#">Paperless</span> | ||||
|   <button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-toggle="collapse" | ||||
|     data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation"> | ||||
|     <span class="navbar-toggler-icon"></span> | ||||
|   </button> | ||||
|   <form (ngSubmit)="search()" class="w-100"> | ||||
|     <input class="form-control form-control-dark" type="text" placeholder="Search" aria-label="Search" | ||||
|       [formControl]="searchField"> | ||||
|   </form> | ||||
|   <ul class="navbar-nav px-3"> | ||||
|     <li class="nav-item text-nowrap"> | ||||
|       <a class="nav-link" (click)="logout()" style="cursor: pointer;"> | ||||
|         <svg class="buttonicon" fill="currentColor"> | ||||
|           <use xlink:href="assets/bootstrap-icons.svg#door-closed"/> | ||||
|         </svg> | ||||
|         Logout | ||||
|       </a> | ||||
|     </li> | ||||
|   </ul> | ||||
| </nav> | ||||
|  | ||||
| <div class="container-fluid"> | ||||
|   <div class="row"> | ||||
|     <nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse"> | ||||
|       <div class="sidebar-sticky pt-3"> | ||||
|         <ul class="nav flex-column"> | ||||
|           <li class="nav-item"> | ||||
|             <a class="nav-link" routerLink="dashboard" routerLinkActive="active"> | ||||
|               <svg class="sidebaricon" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#house"/> | ||||
|               </svg> | ||||
|               Dashboard | ||||
|             </a> | ||||
|           </li> | ||||
|           <li class="nav-item"> | ||||
|             <a class="nav-link" routerLink="documents" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}"> | ||||
|               <svg class="sidebaricon" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#files"/> | ||||
|               </svg> | ||||
|               Documents | ||||
|             </a> | ||||
|           </li> | ||||
|         </ul> | ||||
|  | ||||
|         <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted" *ngIf='openDocuments.length > 0'> | ||||
|           <span>Open documents</span> | ||||
|         </h6> | ||||
|         <ul class="nav flex-column mb-2"> | ||||
|           <li class="nav-item" *ngFor='let d of openDocuments'> | ||||
|             <a class="nav-link" routerLink="documents/{{d.id}}" routerLinkActive="active"> | ||||
|               <svg class="sidebaricon" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#file-text"/> | ||||
|               </svg> | ||||
|               {{d.title}} | ||||
|             </a> | ||||
|           </li> | ||||
|         </ul> | ||||
|  | ||||
|         <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted"> | ||||
|           <span>Manage</span> | ||||
|         </h6> | ||||
|         <ul class="nav flex-column mb-2"> | ||||
|           <li class="nav-item"> | ||||
|             <a class="nav-link" routerLink="correspondents" routerLinkActive="active"> | ||||
|               <svg class="sidebaricon" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#person"/> | ||||
|               </svg> | ||||
|               Correspondents | ||||
|             </a> | ||||
|           </li> | ||||
|           <li class="nav-item"> | ||||
|             <a class="nav-link" routerLink="tags" routerLinkActive="active"> | ||||
|               <svg class="sidebaricon" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#tags"/> | ||||
|               </svg> | ||||
|               Tags | ||||
|             </a> | ||||
|           </li> | ||||
|           <li class="nav-item"> | ||||
|             <a class="nav-link" routerLink="documenttypes" routerLinkActive="active"> | ||||
|               <svg class="sidebaricon" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#hash"/> | ||||
|               </svg> | ||||
|               Document types | ||||
|             </a> | ||||
|           </li> | ||||
|           <li class="nav-item"> | ||||
|             <a class="nav-link" routerLink="logs" routerLinkActive="active"> | ||||
|               <svg class="sidebaricon" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#text-left"/> | ||||
|               </svg> | ||||
|               Logs | ||||
|             </a> | ||||
|           </li> | ||||
|           <li class="nav-item"> | ||||
|             <a class="nav-link" routerLink="settings" routerLinkActive="active"> | ||||
|               <svg class="sidebaricon" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#gear"/> | ||||
|               </svg> | ||||
|               Settings | ||||
|             </a> | ||||
|           </li> | ||||
|         </ul> | ||||
|       </div> | ||||
|     </nav> | ||||
|  | ||||
|     <main role="main" class="col-md-9 ml-sm-auto col-lg-10 px-md-4"> | ||||
|       <router-outlet></router-outlet> | ||||
|     </main> | ||||
|   </div> | ||||
| </div> | ||||
| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { AppFrameComponent } from './app-frame.component'; | ||||
|  | ||||
| describe('AppFrameComponent', () => { | ||||
|   let component: AppFrameComponent; | ||||
|   let fixture: ComponentFixture<AppFrameComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ AppFrameComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(AppFrameComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										41
									
								
								src-ui/src/app/components/app-frame/app-frame.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src-ui/src/app/components/app-frame/app-frame.component.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| import { Component, OnDestroy, OnInit } from '@angular/core'; | ||||
| import { FormControl } from '@angular/forms'; | ||||
| import { Router } from '@angular/router'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| import { PaperlessDocument } from 'src/app/data/paperless-document'; | ||||
| import { AuthService } from 'src/app/services/auth.service'; | ||||
| import { OpenDocumentsService } from 'src/app/services/open-documents.service'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-app-frame', | ||||
|   templateUrl: './app-frame.component.html', | ||||
|   styleUrls: ['./app-frame.component.css'] | ||||
| }) | ||||
| export class AppFrameComponent implements OnInit, OnDestroy { | ||||
|  | ||||
|   constructor (public router: Router, private openDocumentsService: OpenDocumentsService, private authService: AuthService) { | ||||
|   } | ||||
|  | ||||
|   searchField = new FormControl('') | ||||
|  | ||||
|   openDocuments: PaperlessDocument[] = [] | ||||
|  | ||||
|   openDocumentsSubscription: Subscription | ||||
|  | ||||
|   search() { | ||||
|     this.router.navigate(['search'], {queryParams: {query: this.searchField.value}}) | ||||
|   } | ||||
|  | ||||
|   logout() { | ||||
|     this.authService.logout() | ||||
|   } | ||||
|  | ||||
|   ngOnInit() { | ||||
|     this.openDocumentsSubscription = this.openDocumentsService.getOpenDocuments().subscribe(docs => this.openDocuments = docs) | ||||
|   } | ||||
|  | ||||
|   ngOnDestroy() { | ||||
|     this.openDocumentsSubscription.unsubscribe() | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,14 @@ | ||||
|     <div class="modal-header"> | ||||
|       <h4 class="modal-title" id="modal-basic-title">{{title}}</h4> | ||||
|       <button type="button" class="close" aria-label="Close" (click)="cancelClicked()"> | ||||
|         <span aria-hidden="true">×</span> | ||||
|       </button> | ||||
|     </div> | ||||
|     <div class="modal-body"> | ||||
|       <p><b>{{message}}</b></p> | ||||
|       <p *ngIf="message2">{{message2}}</p> | ||||
|     </div> | ||||
|     <div class="modal-footer"> | ||||
|       <button type="button" class="btn btn-outline-dark" (click)="cancelClicked()">Cancel</button> | ||||
|       <button type="button" class="btn btn-danger" (click)="deleteClicked.emit()">Delete</button> | ||||
|     </div> | ||||
| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { DeleteDialogComponent } from './delete-dialog.component'; | ||||
|  | ||||
| describe('DeleteDialogComponent', () => { | ||||
|   let component: DeleteDialogComponent; | ||||
|   let fixture: ComponentFixture<DeleteDialogComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ DeleteDialogComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(DeleteDialogComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,31 @@ | ||||
| import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; | ||||
| import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-delete-dialog', | ||||
|   templateUrl: './delete-dialog.component.html', | ||||
|   styleUrls: ['./delete-dialog.component.css'] | ||||
| }) | ||||
| export class DeleteDialogComponent implements OnInit { | ||||
|  | ||||
|   constructor(public activeModal: NgbActiveModal) { } | ||||
|  | ||||
|   @Output() | ||||
|   public deleteClicked = new EventEmitter() | ||||
|  | ||||
|   @Input() | ||||
|   title = "Delete confirmation" | ||||
|  | ||||
|   @Input() | ||||
|   message = "Do you really want to delete this?" | ||||
|  | ||||
|   @Input() | ||||
|   message2 | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|   } | ||||
|  | ||||
|   cancelClicked() { | ||||
|     this.activeModal.close() | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { EditDialogComponent } from './edit-dialog.component'; | ||||
|  | ||||
| describe('EditDialogComponent', () => { | ||||
|   let component: EditDialogComponent; | ||||
|   let fixture: ComponentFixture<EditDialogComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ EditDialogComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(EditDialogComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,71 @@ | ||||
| import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core'; | ||||
| import { Form, FormGroup } from '@angular/forms'; | ||||
| import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; | ||||
| import { Observable } from 'rxjs'; | ||||
| import { ObjectWithId } from 'src/app/data/object-with-id'; | ||||
| import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'; | ||||
| import { Toast, ToastService } from 'src/app/services/toast.service'; | ||||
|  | ||||
| @Directive() | ||||
| export abstract class EditDialogComponent<T extends ObjectWithId> implements OnInit { | ||||
|  | ||||
|   constructor( | ||||
|     private service: AbstractPaperlessService<T>, | ||||
|     private activeModal: NgbActiveModal, | ||||
|     private toastService: ToastService, | ||||
|     private entityName: string) { } | ||||
|  | ||||
|   @Input() | ||||
|   dialogMode: string = 'create' | ||||
|  | ||||
|   @Input() | ||||
|   object: T | ||||
|  | ||||
|   @Output() | ||||
|   success = new EventEmitter() | ||||
|  | ||||
|   abstract getForm(): FormGroup | ||||
|  | ||||
|   objectForm: FormGroup = this.getForm() | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|     if (this.object != null) { | ||||
|       this.objectForm.patchValue(this.object) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   getTitle() { | ||||
|     switch (this.dialogMode) { | ||||
|       case 'create': | ||||
|         return "Create new " + this.entityName | ||||
|       case 'edit': | ||||
|         return "Edit " + this.entityName | ||||
|       default: | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   save() { | ||||
|     var newObject = Object.assign(Object.assign({}, this.object), this.objectForm.value) | ||||
|     var serverResponse: Observable<T> | ||||
|     switch (this.dialogMode) { | ||||
|       case 'create': | ||||
|         serverResponse = this.service.create(newObject) | ||||
|         break; | ||||
|       case 'edit': | ||||
|         serverResponse = this.service.update(newObject) | ||||
|       default: | ||||
|         break; | ||||
|     } | ||||
|     serverResponse.subscribe(result => { | ||||
|       this.activeModal.close() | ||||
|       this.success.emit(result) | ||||
|     }, error => { | ||||
|       this.toastService.showToast(Toast.make("Error", `Could not save ${this.entityName}: ${error.error.name}`)) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   cancel() { | ||||
|     this.activeModal.close() | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,6 @@ | ||||
| <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"> | ||||
|   <h1 class="h2">{{title}}</h1> | ||||
|   <div class="btn-toolbar mb-2 mb-md-0"> | ||||
|     <ng-content></ng-content> | ||||
|   </div> | ||||
| </div> | ||||
| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { PageHeaderComponent } from './page-header.component'; | ||||
|  | ||||
| describe('PageHeaderComponent', () => { | ||||
|   let component: PageHeaderComponent; | ||||
|   let fixture: ComponentFixture<PageHeaderComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ PageHeaderComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(PageHeaderComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,18 @@ | ||||
| import { Component, Input, OnInit } from '@angular/core'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-page-header', | ||||
|   templateUrl: './page-header.component.html', | ||||
|   styleUrls: ['./page-header.component.css'] | ||||
| }) | ||||
| export class PageHeaderComponent implements OnInit { | ||||
|  | ||||
|   constructor() { } | ||||
|  | ||||
|   @Input() | ||||
|   title: string = "" | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										2
									
								
								src-ui/src/app/components/common/tag/tag.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src-ui/src/app/components/common/tag/tag.component.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| <span *ngIf="!clickable" class="badge" [style.background]="getColour().value" [style.color]="getColour().textColor">{{tag.name}}</span> | ||||
| <a [routerLink]="" *ngIf="clickable" class="badge" [style.background]="getColour().value" [style.color]="getColour().textColor">{{tag.name}}</a> | ||||
							
								
								
									
										25
									
								
								src-ui/src/app/components/common/tag/tag.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src-ui/src/app/components/common/tag/tag.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { TagComponent } from './tag.component'; | ||||
|  | ||||
| describe('TagComponent', () => { | ||||
|   let component: TagComponent; | ||||
|   let fixture: ComponentFixture<TagComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ TagComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(TagComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										29
									
								
								src-ui/src/app/components/common/tag/tag.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src-ui/src/app/components/common/tag/tag.component.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; | ||||
| import { PaperlessTag } from 'src/app/data/paperless-tag'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-tag', | ||||
|   templateUrl: './tag.component.html', | ||||
|   styleUrls: ['./tag.component.css'] | ||||
| }) | ||||
| export class TagComponent implements OnInit { | ||||
|  | ||||
|   constructor() { } | ||||
|  | ||||
|   @Input() | ||||
|   tag: PaperlessTag | ||||
|  | ||||
|   @Input() | ||||
|   clickable: boolean = false | ||||
|  | ||||
|   @Output() | ||||
|   click = new EventEmitter() | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|   } | ||||
|  | ||||
|   getColour() { | ||||
|     return PaperlessTag.COLOURS.find(c => c.id == this.tag.colour) | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,7 @@ | ||||
| :host { | ||||
|   position: fixed; | ||||
|   top: 0; | ||||
|   right: 0; | ||||
|   margin: 0.5em; | ||||
|   z-index: 1200; | ||||
| } | ||||
| @@ -0,0 +1,6 @@ | ||||
| <ngb-toast | ||||
|   *ngFor="let toast of toasts" | ||||
|   [header]="toast.title" [autohide]="true" [delay]="toast.delay" | ||||
|   (hide)="toastService.closeToast(toast)"> | ||||
|   {{toast.content}} | ||||
| </ngb-toast> | ||||
| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { ToastsComponent } from './toasts.component'; | ||||
|  | ||||
| describe('ToastsComponent', () => { | ||||
|   let component: ToastsComponent; | ||||
|   let fixture: ComponentFixture<ToastsComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ ToastsComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(ToastsComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										26
									
								
								src-ui/src/app/components/common/toasts/toasts.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src-ui/src/app/components/common/toasts/toasts.component.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| import { Component, OnDestroy, OnInit } from '@angular/core'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| import { Toast, ToastService } from 'src/app/services/toast.service'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-toasts', | ||||
|   templateUrl: './toasts.component.html', | ||||
|   styleUrls: ['./toasts.component.css'] | ||||
| }) | ||||
| export class ToastsComponent implements OnInit, OnDestroy { | ||||
|  | ||||
|   constructor(private toastService: ToastService) { } | ||||
|  | ||||
|   subscription: Subscription | ||||
|  | ||||
|   toasts: Toast[] = [] | ||||
|  | ||||
|   ngOnDestroy(): void { | ||||
|     this.subscription.unsubscribe() | ||||
|   } | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|     this.subscription = this.toastService.getToasts().subscribe(toasts => this.toasts = toasts) | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										29
									
								
								src-ui/src/app/components/dashboard/dashboard.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src-ui/src/app/components/dashboard/dashboard.component.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
|  | ||||
| <app-page-header title="Dashboard"> | ||||
|   <button type="button" class="btn btn-sm btn-outline-secondary"> | ||||
|     Show Documents with Tag | ||||
|   </button> | ||||
| </app-page-header> | ||||
|  | ||||
| <p>This space for rent...</p> | ||||
|  | ||||
| <div class='row'> | ||||
|   <div class="col"> | ||||
|     <h4>Recent Documents</h4> | ||||
|     <p>Recent docs</p> | ||||
|   </div> | ||||
| </div> | ||||
| <div class='row'> | ||||
|   <div class="col"> | ||||
|     <h4>Documents with tag <span class="badge badge-danger">TODO</span></h4> | ||||
|     <p>...</p> | ||||
|   </div> | ||||
| </div> | ||||
| <div class='row'> | ||||
|   <div class="col-4"> | ||||
|     <h4>Statistics</h4> | ||||
|   </div> | ||||
|   <div class="col-8"> | ||||
|     <h4>Upload new Document</h4> | ||||
|   </div> | ||||
| </div> | ||||
| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { DashboardComponent } from './dashboard.component'; | ||||
|  | ||||
| describe('DashboardComponent', () => { | ||||
|   let component: DashboardComponent; | ||||
|   let fixture: ComponentFixture<DashboardComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ DashboardComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(DashboardComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										15
									
								
								src-ui/src/app/components/dashboard/dashboard.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src-ui/src/app/components/dashboard/dashboard.component.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-dashboard', | ||||
|   templateUrl: './dashboard.component.html', | ||||
|   styleUrls: ['./dashboard.component.css'] | ||||
| }) | ||||
| export class DashboardComponent implements OnInit { | ||||
|  | ||||
|   constructor() { } | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,124 @@ | ||||
| <app-page-header [(title)]="title"> | ||||
|     <button type="button" class="btn btn-sm btn-outline-danger mr-2" (click)="delete()"> | ||||
|         <svg class="buttonicon" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#trash" /> | ||||
|         </svg> | ||||
|         Delete | ||||
|     </button> | ||||
|     <a [href]="downloadUrl" class="btn btn-sm btn-outline-secondary mr-2"> | ||||
|         <svg class="buttonicon" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#download" /> | ||||
|         </svg> | ||||
|         Download | ||||
|     </a> | ||||
|     <button type="button" class="btn btn-sm btn-outline-secondary" (click)="close()"> | ||||
|         <svg class="buttonicon" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#x" /> | ||||
|         </svg> | ||||
|         Close | ||||
|     </button> | ||||
| </app-page-header> | ||||
|  | ||||
|  | ||||
| <div class="row"> | ||||
|     <div class="col-xl"> | ||||
|         <form [formGroup]='documentForm' (ngSubmit)="save()"> | ||||
|             <div class="form-group"> | ||||
|                 <label for="title">Title</label> | ||||
|                 <input type="text" class="form-control" id="title" formControlName='title'> | ||||
|             </div> | ||||
|             <div class="form-group"> | ||||
|                 <label for="archive_serial_number">Archive Serial Number</label> | ||||
|                 <input type="number" class="form-control" id="archive_serial_number" | ||||
|                     formControlName='archive_serial_number'> | ||||
|             </div> | ||||
|             <div class="form-row"> | ||||
|                 <div class="form-group col"> | ||||
|                     <label for="created_date">Date created</label> | ||||
|                     <input type="date" class="form-control" id="created_date" formControlName='created_date'> | ||||
|                 </div> | ||||
|                 <div class="form-group col"> | ||||
|                     <label for="created_time">Time created</label> | ||||
|                     <input type="time" class="form-control" id="created_time" formControlName='created_time'> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="form-group"> | ||||
|                 <label for="content">Content</label> | ||||
|                 <textarea class="form-control" id="content" rows="5" formControlName='content'></textarea> | ||||
|             </div> | ||||
|             <div class="form-group"> | ||||
|                 <label for="correspondent">Correspondent</label> | ||||
|                 <div class="input-group"> | ||||
|                     <select class="form-control" id="correspondent" formControlName="correspondent_id"> | ||||
|                         <option [ngValue]="null">---</option> | ||||
|                         <option *ngFor='let c of correspondents' [ngValue]="c.id">{{c.name}}</option> | ||||
|                     </select> | ||||
|                     <div class="input-group-append"> | ||||
|                         <button class="btn btn-outline-secondary" type="button" (click)="createCorrespondent()"> | ||||
|                             <svg class="buttonicon" fill="currentColor"> | ||||
|                                 <use xlink:href="assets/bootstrap-icons.svg#plus" /> | ||||
|                             </svg> | ||||
|                         </button> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="form-group"> | ||||
|                 <label for="document_type">Document Type</label> | ||||
|                 <div class="input-group"> | ||||
|                     <select class="form-control" id="document_type" formControlName="document_type_id"> | ||||
|                         <option [ngValue]="null">---</option> | ||||
|                         <option *ngFor="let dt of documentTypes" [ngValue]="dt.id">{{dt.name}}</option> | ||||
|                     </select> | ||||
|                     <div class="input-group-append"> | ||||
|                         <button class="btn btn-outline-secondary" type="button" (click)="createDocumentType()"> | ||||
|                             <svg class="buttonicon" fill="currentColor"> | ||||
|                                 <use xlink:href="assets/bootstrap-icons.svg#plus" /> | ||||
|                             </svg> | ||||
|                         </button> | ||||
|                     </div> | ||||
|                 </div> | ||||
|  | ||||
|             </div> | ||||
|             <div class="form-group"> | ||||
|                 <label for="exampleFormControlTextarea1">Tags</label> | ||||
|  | ||||
|                 <div class="input-group"> | ||||
|                     <div class="form-control"> | ||||
|                         <span *ngFor="let tag of documentForm.value.tags"> | ||||
|                             <app-tag [tag]="tag" [clickable]="true" (click)="removeTag(tag)"></app-tag> | ||||
|                               | ||||
|                         </span> | ||||
|                     </div> | ||||
|  | ||||
|                     <div class="input-group-append" ngbDropdown> | ||||
|                         <button class="btn btn-outline-secondary" type="button" ngbDropdownToggle></button> | ||||
|                         <div ngbDropdownMenu aria-labelledby="dropdownBasic1"> | ||||
|                             <button type="button" *ngFor="let tag of tags" ngbDropdownItem (click)="addTag(tag)"> | ||||
|                                 <app-tag [tag]="tag"></app-tag> | ||||
|                             </button> | ||||
|                         </div> | ||||
|                     </div> | ||||
|  | ||||
|                     <div class="input-group-append"> | ||||
|                         <button class="btn btn-outline-secondary" type="button" (click)="createTag()"> | ||||
|                             <svg class="buttonicon" fill="currentColor"> | ||||
|                                 <use xlink:href="assets/bootstrap-icons.svg#plus" /> | ||||
|                             </svg> | ||||
|                         </button> | ||||
|                     </div> | ||||
|  | ||||
|                 </div> | ||||
|             </div> | ||||
|             <button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()">Save & edit next</button>  | ||||
|             <button type="submit" class="btn btn-primary">Save</button>  | ||||
|         </form> | ||||
|     </div> | ||||
|  | ||||
|     <div class="col-xl"> | ||||
|         <object [data]="previewUrl | safe" type="application/pdf" width="100%" height="100%"> | ||||
|             <p>Your browser does not support PDFs. | ||||
|                 <a href="previewUrl">Download the PDF</a>.</p> | ||||
|         </object> | ||||
|  | ||||
|     </div> | ||||
| </div> | ||||
| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { DocumentDetailComponent } from './document-detail.component'; | ||||
|  | ||||
| describe('DocumentDetailComponent', () => { | ||||
|   let component: DocumentDetailComponent; | ||||
|   let fixture: ComponentFixture<DocumentDetailComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ DocumentDetailComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(DocumentDetailComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,177 @@ | ||||
| import { DatePipe } from '@angular/common'; | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import { FormControl, FormGroup } from '@angular/forms'; | ||||
| import { ActivatedRoute, Router } from '@angular/router'; | ||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; | ||||
| import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'; | ||||
| import { PaperlessDocument } from 'src/app/data/paperless-document'; | ||||
| import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'; | ||||
| import { PaperlessTag } from 'src/app/data/paperless-tag'; | ||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service'; | ||||
| import { OpenDocumentsService } from 'src/app/services/open-documents.service'; | ||||
| import { CorrespondentService } from 'src/app/services/rest/correspondent.service'; | ||||
| import { DocumentTypeService } from 'src/app/services/rest/document-type.service'; | ||||
| import { DocumentService } from 'src/app/services/rest/document.service'; | ||||
| import { TagService } from 'src/app/services/rest/tag.service'; | ||||
| import { DeleteDialogComponent } from '../common/delete-dialog/delete-dialog.component'; | ||||
| import { CorrespondentEditDialogComponent } from '../manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component'; | ||||
| import { DocumentTypeEditDialogComponent } from '../manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component'; | ||||
| import { TagEditDialogComponent } from '../manage/tag-list/tag-edit-dialog/tag-edit-dialog.component'; | ||||
| @Component({ | ||||
|   selector: 'app-document-detail', | ||||
|   templateUrl: './document-detail.component.html', | ||||
|   styleUrls: ['./document-detail.component.css'] | ||||
| }) | ||||
| export class DocumentDetailComponent implements OnInit { | ||||
|  | ||||
|   documentId: number | ||||
|   document: PaperlessDocument | ||||
|   title: string | ||||
|   previewUrl: string | ||||
|   downloadUrl: string | ||||
|  | ||||
|   correspondents: PaperlessCorrespondent[] | ||||
|   documentTypes: PaperlessDocumentType[] | ||||
|   tags: PaperlessTag[] | ||||
|  | ||||
|   documentForm: FormGroup = new FormGroup({ | ||||
|     title: new FormControl(''), | ||||
|     content: new FormControl(''), | ||||
|     created_date: new FormControl(), | ||||
|     created_time: new FormControl(), | ||||
|     correspondent_id: new FormControl(), | ||||
|     document_type_id: new FormControl(), | ||||
|     archive_serial_number: new FormControl(), | ||||
|     tags: new FormControl([]) | ||||
|   }) | ||||
|  | ||||
|   constructor( | ||||
|     private documentsService: DocumentService,  | ||||
|     private route: ActivatedRoute, | ||||
|     private correspondentService: CorrespondentService, | ||||
|     private documentTypeService: DocumentTypeService, | ||||
|     private tagService: TagService, | ||||
|     private datePipe: DatePipe, | ||||
|     private router: Router, | ||||
|     private modalService: NgbModal, | ||||
|     private openDocumentService: OpenDocumentsService, | ||||
|     private documentListViewService: DocumentListViewService) { } | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|     this.correspondentService.list().subscribe(result => this.correspondents = result.results) | ||||
|     this.documentTypeService.list().subscribe(result => this.documentTypes = result.results) | ||||
|     this.tagService.list().subscribe(result => this.tags = result.results) | ||||
|  | ||||
|     this.route.paramMap.subscribe(paramMap => { | ||||
|       this.documentId = +paramMap.get('id') | ||||
|       this.previewUrl = this.documentsService.getPreviewUrl(this.documentId) | ||||
|       this.downloadUrl = this.documentsService.getDownloadUrl(this.documentId) | ||||
|       this.documentsService.get(this.documentId).subscribe(doc => { | ||||
|         this.openDocumentService.openDocument(doc) | ||||
|         this.document = doc | ||||
|         this.title = doc.title | ||||
|         this.documentForm.patchValue(doc) | ||||
|         this.documentForm.get('created_date').patchValue(this.datePipe.transform(doc.created, 'yyyy-MM-dd')) | ||||
|         this.documentForm.get('created_time').patchValue(this.datePipe.transform(doc.created, 'HH:mm:ss')) | ||||
|       }, error => {this.router.navigate(['404'])}) | ||||
|     }) | ||||
|  | ||||
|   } | ||||
|  | ||||
|   createTag() { | ||||
|     var modal = this.modalService.open(TagEditDialogComponent, {backdrop: 'static'}) | ||||
|     modal.componentInstance.dialogMode = 'create' | ||||
|     modal.componentInstance.success.subscribe(newTag => { | ||||
|       this.tagService.list().subscribe(tags => { | ||||
|         this.tags = tags.results | ||||
|         this.documentForm.get('tags_id').value.push(newTag.id) | ||||
|       }) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   createDocumentType() { | ||||
|     var modal = this.modalService.open(DocumentTypeEditDialogComponent, {backdrop: 'static'}) | ||||
|     modal.componentInstance.dialogMode = 'create' | ||||
|     modal.componentInstance.success.subscribe(newDocumentType => { | ||||
|       this.documentTypeService.list().subscribe(documentTypes => { | ||||
|         this.documentTypes = documentTypes.results | ||||
|         this.documentForm.get('document_type_id').setValue(newDocumentType.id) | ||||
|       }) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   createCorrespondent() { | ||||
|     var modal = this.modalService.open(CorrespondentEditDialogComponent, {backdrop: 'static'}) | ||||
|     modal.componentInstance.dialogMode = 'create' | ||||
|     modal.componentInstance.success.subscribe(newCorrespondent => { | ||||
|       this.correspondentService.list().subscribe(correspondents => { | ||||
|         this.correspondents = correspondents.results | ||||
|         this.documentForm.get('correspondent_id').setValue(newCorrespondent.id) | ||||
|       }) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   getTag(id: number): PaperlessTag { | ||||
|     return this.tags.find(tag => tag.id == id) | ||||
|   } | ||||
|  | ||||
|   getColour(id: number) { | ||||
|     return PaperlessTag.COLOURS.find(c => c.id == this.getTag(id).colour) | ||||
|   } | ||||
|  | ||||
|   addTag(id: number) { | ||||
|     if (this.documentForm.value.tags.indexOf(id) == -1) { | ||||
|       this.documentForm.value.tags.push(id) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   removeTag(id: number) { | ||||
|     var index = this.documentForm.value.tags.indexOf(id) | ||||
|     if (index > -1) { | ||||
|       this.documentForm.value.tags.splice(index, 1) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   save() { | ||||
|     var newDocument = Object.assign(Object.assign({}, this.document), this.documentForm.value) | ||||
|     console.log(this.document) | ||||
|     console.log(newDocument) | ||||
|     this.documentsService.update(newDocument).subscribe(result => { | ||||
|       this.close() | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   saveEditNext() { | ||||
|     var newDocument = Object.assign(Object.assign({}, this.document), this.documentForm.value) | ||||
|     this.documentsService.update(newDocument).subscribe(result => { | ||||
|       this.documentListViewService.getNext(this.document.id).subscribe(nextDocId => { | ||||
|         if (nextDocId) { | ||||
|           this.openDocumentService.closeDocument(this.document) | ||||
|           this.router.navigate(['documents', nextDocId]) | ||||
|         } | ||||
|       }) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   close() { | ||||
|     this.openDocumentService.closeDocument(this.document) | ||||
|     this.router.navigate(['documents']) | ||||
|   } | ||||
|  | ||||
|   delete() { | ||||
|     let modal = this.modalService.open(DeleteDialogComponent, {backdrop: 'static'}) | ||||
|     modal.componentInstance.message = `Do you really want to delete document '${this.document.title}'?` | ||||
|     modal.componentInstance.message2 = `The files for this document will be deleted permanently. This operation cannot be undone.` | ||||
|     modal.componentInstance.deleteClicked.subscribe(() => { | ||||
|       this.documentsService.delete(this.document).subscribe(() => { | ||||
|         modal.close()   | ||||
|         this.close() | ||||
|       }) | ||||
|     }) | ||||
|  | ||||
|   } | ||||
|  | ||||
|   hasNext() { | ||||
|     return this.documentListViewService.hasNext(this.documentId) | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,11 @@ | ||||
| .result-content { | ||||
|   color: darkgray; | ||||
| } | ||||
|  | ||||
| .doc-img { | ||||
|   object-fit: cover; | ||||
|   object-position: top; | ||||
|   height: 100%; | ||||
|   position: absolute; | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,38 @@ | ||||
| <div class="card mb-3 bg-light"> | ||||
|   <div class="row no-gutters"> | ||||
|     <div class="col-md-2 d-none d-lg-block"> | ||||
|       <img [src]="getThumbUrl()" class="card-img doc-img"> | ||||
|     </div> | ||||
|     <div class="col"> | ||||
|       <div class="card-body"> | ||||
|  | ||||
|         <h5 class="card-title">{{document.title}}<app-tag [tag]="t" *ngFor="let t of document.tags" class="ml-1"></app-tag></h5> | ||||
|         <p class="card-text"> | ||||
|           <app-result-hightlight *ngIf="getDetailsAsHighlight()" class="result-content" [highlights]="getDetailHighlight()"></app-result-hightlight> | ||||
|           <span *ngIf="getDetailsAsString()" class="result-content">{{getDetailsString()}}</span> | ||||
|         </p> | ||||
|  | ||||
|  | ||||
|         <div class="d-flex justify-content-between align-items-center"> | ||||
|           <div class="btn-group"> | ||||
|             <a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary"> | ||||
|               <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | ||||
|                 <path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/> | ||||
|               </svg> | ||||
|               Edit | ||||
|             </a> | ||||
|             <a type="button" class="btn btn-sm btn-outline-secondary" [href]="getDownloadUrl()"> | ||||
|               <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-download" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | ||||
|                 <path fill-rule="evenodd" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/> | ||||
|                 <path fill-rule="evenodd" d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/> | ||||
|               </svg> | ||||
|               Download | ||||
|             </a> | ||||
|           </div> | ||||
|           <small class="text-muted">{{document.created | date}}</small> | ||||
|         </div> | ||||
|  | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { DocumentCardLargeComponent } from './document-card-large.component'; | ||||
|  | ||||
| describe('DocumentCardLargeComponent', () => { | ||||
|   let component: DocumentCardLargeComponent; | ||||
|   let fixture: ComponentFixture<DocumentCardLargeComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ DocumentCardLargeComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(DocumentCardLargeComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,46 @@ | ||||
| import { Component, Input, OnInit } from '@angular/core'; | ||||
| import { DomSanitizer } from '@angular/platform-browser'; | ||||
| import { PaperlessDocument } from 'src/app/data/paperless-document'; | ||||
| import { PaperlessTag } from 'src/app/data/paperless-tag'; | ||||
| import { DocumentService } from 'src/app/services/rest/document.service'; | ||||
| import { SearchResultHighlightedText } from 'src/app/services/rest/search.service'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-document-card-large', | ||||
|   templateUrl: './document-card-large.component.html', | ||||
|   styleUrls: ['./document-card-large.component.css'] | ||||
| }) | ||||
| export class DocumentCardLargeComponent implements OnInit { | ||||
|  | ||||
|   constructor(private documentService: DocumentService, private sanitizer: DomSanitizer) { } | ||||
|  | ||||
|   @Input() | ||||
|   document: PaperlessDocument | ||||
|  | ||||
|   @Input() | ||||
|   details: any | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|   } | ||||
|  | ||||
|   getDetailsAsString() { | ||||
|     if (typeof this.details === 'string') { | ||||
|       return this.details.substring(0, 500) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   getDetailsAsHighlight() { | ||||
|     //TODO: this is not an exact typecheck, can we do better | ||||
|     if (this.details instanceof Array) { | ||||
|       return this.details | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   getThumbUrl() { | ||||
|     return this.documentService.getThumbUrl(this.document.id) | ||||
|   } | ||||
|  | ||||
|   getDownloadUrl() { | ||||
|     return this.documentService.getDownloadUrl(this.document.id) | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,5 @@ | ||||
| .doc-img { | ||||
|   object-fit: cover; | ||||
|   object-position: top; | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,26 @@ | ||||
| <div class="col-auto mb-3"> | ||||
|   <div class="card h-100 bg-light" style="width: 14rem"> | ||||
|     <div style="height: 10rem; overflow: hidden;"> | ||||
|       <img [src]="getThumbUrl()" class="card-img doc-img"/> | ||||
|     </div> | ||||
|     <div class="card-body"> | ||||
|       <p class="card-title">{{document.correspondent ? document.correspondent.name + ': ' : ''}}{{document.title}} <app-tag [tag]="t" *ngFor="let t of document.tags" class="ml-1"></app-tag></p> | ||||
|       <div class="d-flex justify-content-between align-items-center"> | ||||
|         <div class="btn-group"> | ||||
|           <a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary"> | ||||
|             <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | ||||
|               <path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/> | ||||
|             </svg> | ||||
|           </a> | ||||
|           <a [href]="getDownloadUrl()" class="btn btn-sm btn-outline-secondary"> | ||||
|             <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-download" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | ||||
|               <path fill-rule="evenodd" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/> | ||||
|               <path fill-rule="evenodd" d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/> | ||||
|             </svg> | ||||
|           </a> | ||||
|         </div> | ||||
|         <small class="text-muted">{{document.created | date}}</small> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { DocumentCardSmallComponent } from './document-card-small.component'; | ||||
|  | ||||
| describe('DocumentCardSmallComponent', () => { | ||||
|   let component: DocumentCardSmallComponent; | ||||
|   let fixture: ComponentFixture<DocumentCardSmallComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ DocumentCardSmallComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(DocumentCardSmallComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,27 @@ | ||||
| import { Component, Input, OnInit } from '@angular/core'; | ||||
| import { PaperlessDocument } from 'src/app/data/paperless-document'; | ||||
| import { DocumentService } from 'src/app/services/rest/document.service'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-document-card-small', | ||||
|   templateUrl: './document-card-small.component.html', | ||||
|   styleUrls: ['./document-card-small.component.css'] | ||||
| }) | ||||
| export class DocumentCardSmallComponent implements OnInit { | ||||
|  | ||||
|   constructor(private documentService: DocumentService) { } | ||||
|  | ||||
|   @Input() | ||||
|   document: PaperlessDocument | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|   } | ||||
|  | ||||
|   getThumbUrl() { | ||||
|     return this.documentService.getThumbUrl(this.document.id) | ||||
|   } | ||||
|  | ||||
|   getDownloadUrl() { | ||||
|     return this.documentService.getDownloadUrl(this.document.id) | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,94 @@ | ||||
| <app-page-header title="Documents"> | ||||
|  | ||||
|   <div class="btn-group btn-group-toggle mr-2" ngbRadioGroup [(ngModel)]="displayMode" (ngModelChange)="saveDisplayMode()"> | ||||
|     <label ngbButtonLabel class="btn-outline-secondary btn-sm"> | ||||
|       <input ngbButton type="radio" class="btn btn-sm" value="details"> | ||||
|       <svg class="toolbaricon" fill="currentColor"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#list-ul"/> | ||||
|       </svg> | ||||
|     </label> | ||||
|     <label ngbButtonLabel class="btn-outline-secondary btn-sm"> | ||||
|       <input ngbButton type="radio" class="btn btn-sm" value="smallCards"> | ||||
|       <svg class="toolbaricon" fill="currentColor"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#grid"/> | ||||
|       </svg> | ||||
|     </label> | ||||
|     <label ngbButtonLabel class="btn-outline-secondary btn-sm"> | ||||
|       <input ngbButton type="radio" class="btn btn-sm" value="largeCards"> | ||||
|       <svg class="toolbaricon" fill="currentColor"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#hdd-stack"/> | ||||
|       </svg> | ||||
|     </label> | ||||
|   </div> | ||||
|   <div class="btn-group btn-group-toggle mr-2" ngbRadioGroup [(ngModel)]="docs.currentSortDirection" (ngModelChange)="reload()"> | ||||
|     <div ngbDropdown class="btn-group"> | ||||
|       <button class="btn btn-outline-secondary btn-sm" id="dropdownBasic1" ngbDropdownToggle>Sort by</button> | ||||
|       <div ngbDropdownMenu aria-labelledby="dropdownBasic1"> | ||||
|         <button *ngFor="let f of getSortFields()" ngbDropdownItem (click)="setSort(f.field)">{{f.name}}</button> | ||||
|       </div> | ||||
|     </div> | ||||
|     <label ngbButtonLabel class="btn-outline-secondary btn-sm"> | ||||
|       <input ngbButton type="radio" class="btn btn-sm" value="asc"> | ||||
|       <svg class="toolbaricon" fill="currentColor"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#sort-alpha-down"/> | ||||
|       </svg> | ||||
|     </label> | ||||
|     <label ngbButtonLabel class="btn-outline-secondary btn-sm"> | ||||
|       <input ngbButton type="radio" class="btn btn-sm" value="des"> | ||||
|       <svg class="toolbaricon" fill="currentColor"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#sort-alpha-up-alt"/> | ||||
|       </svg> | ||||
|     </label> | ||||
|   </div> | ||||
|   <button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" (click)="showFilter=!showFilter"> | ||||
|     <svg class="toolbaricon" fill="currentColor"> | ||||
|       <use xlink:href="assets/bootstrap-icons.svg#funnel"/> | ||||
|     </svg> | ||||
|     Filter | ||||
|   </button> | ||||
| </app-page-header> | ||||
|  | ||||
| <div class="card w-100 mb-3" [hidden]="!showFilter"> | ||||
|   <div class="card-body"> | ||||
|     <h5 class="card-title">Filter</h5> | ||||
|     <app-filter-editor [(ruleSet)]="filter" (apply)="applyFilter()"></app-filter-editor> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
| <ngb-pagination [pageSize]="25" [collectionSize]="docs.collectionSize" [(page)]="docs.currentPage" (pageChange)="reload()" | ||||
|   aria-label="Default pagination"></ngb-pagination> | ||||
|  | ||||
| <div *ngIf="displayMode == 'largeCards'"> | ||||
|   <app-document-card-large *ngFor="let d of docs.documents" | ||||
|     [document]="d" | ||||
|     [details]="d.content"> | ||||
|   </app-document-card-large> | ||||
| </div> | ||||
|  | ||||
| <table class="table table-striped table-sm" *ngIf="displayMode == 'details'"> | ||||
|   <thead> | ||||
|     <th>ASN</th> | ||||
|     <th>Correspondent</th> | ||||
|     <th>Title</th> | ||||
|     <th>Document type</th> | ||||
|     <th>Date created</th> | ||||
|     <th>Date added</th> | ||||
|   </thead> | ||||
|   <tbody> | ||||
|     <tr *ngFor="let d of docs.documents" routerLink="/documents/{{d.id}}"> | ||||
|       <td>{{d.archive_serial_number}}</td> | ||||
|       <td>{{d.correspondent ? d.correspondent.name : ''}}</td> | ||||
|       <td>{{d.title}}<app-tag [tag]="t" *ngFor="let t of d.tags" class="ml-1"></app-tag> | ||||
|       </td> | ||||
|       <td>{{d.document_type ? d.document_type.name : ''}}</td> | ||||
|       <td>{{d.created | date}}</td> | ||||
|       <td>{{d.added | date}}</td> | ||||
|     </tr> | ||||
|   </tbody> | ||||
| </table> | ||||
|  | ||||
| <div class="row justify-content-left" *ngIf="displayMode == 'smallCards'"> | ||||
|   <app-document-card-small [document]="d" *ngFor="let d of docs.documents"></app-document-card-small> | ||||
| </div> | ||||
|  | ||||
| <p *ngIf="docs.documents.length == 0" class="mx-auto">No results</p> | ||||
| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { DocumentListComponent } from './document-list.component'; | ||||
|  | ||||
| describe('DocumentListComponent', () => { | ||||
|   let component: DocumentListComponent; | ||||
|   let fixture: ComponentFixture<DocumentListComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ DocumentListComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(DocumentListComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,51 @@ | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service'; | ||||
| import { FilterRuleSet } from '../filter-editor/filter-editor.component'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-document-list', | ||||
|   templateUrl: './document-list.component.html', | ||||
|   styleUrls: ['./document-list.component.css'] | ||||
| }) | ||||
| export class DocumentListComponent implements OnInit { | ||||
|  | ||||
|   constructor( | ||||
|     public docs: DocumentListViewService) { } | ||||
|  | ||||
|   displayMode = 'smallCards' // largeCards, smallCards, details | ||||
|  | ||||
|   filter = new FilterRuleSet() | ||||
|   showFilter = false | ||||
|  | ||||
|   getSortFields() { | ||||
|     return DocumentListViewService.SORT_FIELDS | ||||
|   } | ||||
|  | ||||
|   setSort(field: string) { | ||||
|     this.docs.currentSortField = field | ||||
|     this.reload() | ||||
|   } | ||||
|  | ||||
|   saveDisplayMode() { | ||||
|     localStorage.setItem('document-list:displayMode', this.displayMode) | ||||
|   } | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|     if (localStorage.getItem('document-list:displayMode') != null) { | ||||
|       this.displayMode = localStorage.getItem('document-list:displayMode') | ||||
|     } | ||||
|     this.filter = this.docs.currentFilter.clone() | ||||
|     this.showFilter = this.filter.rules.length > 0 | ||||
|     this.reload() | ||||
|   } | ||||
|  | ||||
|   reload() { | ||||
|     this.docs.reload() | ||||
|   } | ||||
|  | ||||
|   applyFilter() { | ||||
|     this.docs.setFilter(this.filter.clone()) | ||||
|     this.reload() | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,51 @@ | ||||
| <div *ngFor="let rule of ruleSet.rules" class="form-row form-group"> | ||||
|   <div class="col"> | ||||
|     <select class="form-control form-control-sm" [(ngModel)]="rule.type" (change)="rule.value = null"> | ||||
|       <option *ngFor="let ruleType of getRuleTypes()" [ngValue]="ruleType">{{ruleType.name}}</option> | ||||
|     </select> | ||||
|   </div> | ||||
|   <div class="col"> | ||||
|     <input *ngIf="rule.type.datatype == 'string'" type="text" class="form-control form-control-sm" [(ngModel)]="rule.value"> | ||||
|     <input *ngIf="rule.type.datatype == 'number'" type="number" class="form-control form-control-sm" [(ngModel)]="rule.value"> | ||||
|     <input *ngIf="rule.type.datatype == 'date'" type="date" class="form-control form-control-sm" [(ngModel)]="rule.value"> | ||||
|  | ||||
|     <select *ngIf="rule.type.datatype == 'tag'" class="form-control form-control-sm" [(ngModel)]="rule.value"> | ||||
|       <option *ngFor="let t of tags" [ngValue]="t.id">{{t.name}}</option> | ||||
|     </select> | ||||
|  | ||||
|     <select *ngIf="rule.type.datatype == 'documentType'" class="form-control form-control-sm" [(ngModel)]="rule.value"> | ||||
|       <option *ngFor="let dt of documentTypes" [ngValue]="dt.id">{{dt.name}}</option> | ||||
|     </select> | ||||
|  | ||||
|     <select *ngIf="rule.type.datatype == 'correspondent'" class="form-control form-control-sm" [(ngModel)]="rule.value"> | ||||
|       <option *ngFor="let c of correspondents" [ngValue]="c.id">{{c.name}}</option> | ||||
|     </select> | ||||
|  | ||||
|     <select *ngIf="rule.type.datatype == 'boolean'" class="form-control form-control-sm" [(ngModel)]="rule.value"> | ||||
|       <option [ngValue]="true">Yes</option> | ||||
|       <option [ngValue]="false">No</option> | ||||
|     </select> | ||||
|  | ||||
|   </div> | ||||
|   <div class="col-auto"> | ||||
|     <button class="btn btn-sm btn-outline-secondary" (click)="removeRuleClicked(rule)"> | ||||
|       <svg class="toolbaricon" fill="currentColor"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#x"/> | ||||
|       </svg> | ||||
|     </button> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
| <div class="form-row form-group"> | ||||
|   <div class="col"> | ||||
|     <select [(ngModel)]="selectedRuleType" class="form-control form-control-sm"> | ||||
|       <option *ngFor="let ruleType of getRuleTypes()" [ngValue]="ruleType">{{ruleType.name}}</option> | ||||
|     </select> | ||||
|   </div> | ||||
|   <div class="col-auto"> | ||||
|     <button (click)="newRuleClicked()" class="btn btn-sm btn-outline-secondary">Add</button> | ||||
|   </div> | ||||
|   <div class="col-auto"> | ||||
|     <button (click)="applyClicked()" class="btn btn-sm btn-outline-secondary">Apply</button> | ||||
|   </div> | ||||
| </div> | ||||
| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { FilterEditorComponent } from './filter-editor.component'; | ||||
|  | ||||
| describe('FilterEditorComponent', () => { | ||||
|   let component: FilterEditorComponent; | ||||
|   let fixture: ComponentFixture<FilterEditorComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ FilterEditorComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(FilterEditorComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,118 @@ | ||||
| import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; | ||||
| import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'; | ||||
| import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'; | ||||
| import { PaperlessTag } from 'src/app/data/paperless-tag'; | ||||
| import { CorrespondentService } from 'src/app/services/rest/correspondent.service'; | ||||
| import { DocumentTypeService } from 'src/app/services/rest/document-type.service'; | ||||
| import { TagService } from 'src/app/services/rest/tag.service'; | ||||
|  | ||||
| export interface FilterRuleType { | ||||
|   name: string | ||||
|   filtervar: string | ||||
|   datatype: string //number, string, boolean, date | ||||
| } | ||||
|  | ||||
| export interface FilterRule { | ||||
|   type: FilterRuleType | ||||
|   value: any | ||||
| } | ||||
|  | ||||
| export class FilterRuleSet { | ||||
|  | ||||
|   static RULE_TYPES: FilterRuleType[] = [ | ||||
|     {name: "Title contains", filtervar: "title__icontains", datatype: "string"}, | ||||
|     {name: "Content contains", filtervar: "content__icontains", datatype: "string"}, | ||||
|      | ||||
|     {name: "ASN is", filtervar: "archive_serial_number", datatype: "number"}, | ||||
|      | ||||
|     {name: "Correspondent is", filtervar: "correspondent__id", datatype: "correspondent"}, | ||||
|     {name: "Document type is", filtervar: "document_type__id", datatype: "document_type"}, | ||||
|     {name: "Has tag", filtervar: "tags__id", datatype: "tag"}, | ||||
|      | ||||
|     {name: "Has any tag", filtervar: "is_tagged", datatype: "boolean"}, | ||||
|    | ||||
|     {name: "Date created before", filtervar: "created__date__lt", datatype: "date"}, | ||||
|     {name: "Date created after", filtervar: "created__date__gt", datatype: "date"}, | ||||
|    | ||||
|     {name: "Year created is", filtervar: "created__year", datatype: "number"}, | ||||
|     {name: "Month created is", filtervar: "created__month", datatype: "number"}, | ||||
|     {name: "Day created is", filtervar: "created__day", datatype: "number"}, | ||||
|    | ||||
|     {name: "Date added before", filtervar: "added__date__lt", datatype: "date"}, | ||||
|     {name: "Date added after", filtervar: "added__date__gt", datatype: "date"}, | ||||
|      | ||||
|     {name: "Date modified before", filtervar: "modified__date__lt", datatype: "date"}, | ||||
|     {name: "Date modified after", filtervar: "modified__date__gt", datatype: "date"}, | ||||
|   ] | ||||
|  | ||||
|   rules: FilterRule[] = [] | ||||
|  | ||||
|   toQueryParams() { | ||||
|     let params = {} | ||||
|     for (let rule of this.rules) { | ||||
|       params[rule.type.filtervar] = rule.value | ||||
|     } | ||||
|     return params | ||||
|   } | ||||
|  | ||||
|   clone(): FilterRuleSet { | ||||
|     let newRuleSet = new FilterRuleSet() | ||||
|     for (let rule of this.rules) { | ||||
|       newRuleSet.rules.push({type: rule.type, value: rule.value}) | ||||
|     } | ||||
|     return newRuleSet | ||||
|   } | ||||
|  | ||||
|   constructor() { } | ||||
|  | ||||
| } | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-filter-editor', | ||||
|   templateUrl: './filter-editor.component.html', | ||||
|   styleUrls: ['./filter-editor.component.css'] | ||||
| }) | ||||
| export class FilterEditorComponent implements OnInit { | ||||
|  | ||||
|   constructor(private documentTypeService: DocumentTypeService, private tagService: TagService, private correspondentService: CorrespondentService) { } | ||||
|  | ||||
|   @Input() | ||||
|   ruleSet = new FilterRuleSet() | ||||
|  | ||||
|   @Output() | ||||
|   ruleSetChange = new EventEmitter<FilterRuleSet>() | ||||
|  | ||||
|   @Output() | ||||
|   apply = new EventEmitter() | ||||
|  | ||||
|   selectedRuleType: FilterRuleType = FilterRuleSet.RULE_TYPES[0] | ||||
|  | ||||
|   correspondents: PaperlessCorrespondent[] = [] | ||||
|   tags: PaperlessTag[] = [] | ||||
|   documentTypes: PaperlessDocumentType[] = [] | ||||
|  | ||||
|   newRuleClicked() { | ||||
|     this.ruleSet.rules.push({type: this.selectedRuleType, value: null}) | ||||
|   } | ||||
|  | ||||
|   removeRuleClicked(rule) { | ||||
|     let index = this.ruleSet.rules.findIndex(r => r == rule) | ||||
|     if (index > -1) { | ||||
|       this.ruleSet.rules.splice(index, 1) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   applyClicked() { | ||||
|     this.apply.next() | ||||
|   } | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|     this.correspondentService.list().subscribe(result => {this.correspondents = result.results}) | ||||
|     this.tagService.list().subscribe(result => this.tags = result.results) | ||||
|     this.documentTypeService.list().subscribe(result => this.documentTypes = result.results) | ||||
|   } | ||||
|  | ||||
|   getRuleTypes() { | ||||
|     return FilterRuleSet.RULE_TYPES | ||||
|   } | ||||
| } | ||||
							
								
								
									
										44
									
								
								src-ui/src/app/components/login/login.component.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src-ui/src/app/components/login/login.component.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| .form-signin-container { | ||||
|   top: 0; | ||||
|   bottom: 0; | ||||
|   left: 0; | ||||
|   right: 0; | ||||
|   position: fixed; | ||||
|   background-color: #f5f5f5; | ||||
| } | ||||
|  | ||||
| .form-signin { | ||||
|   max-width: 330px; | ||||
|   height: auto; | ||||
|   position: fixed; | ||||
|   left: 0; | ||||
|   right: 0; | ||||
|   top: 0; | ||||
|   bottom: 0; | ||||
|  | ||||
|   margin: auto; | ||||
|   text-align: center; | ||||
| } | ||||
| .form-signin .checkbox { | ||||
|   font-weight: 400; | ||||
| } | ||||
| .form-signin .form-control { | ||||
|   position: relative; | ||||
|   box-sizing: border-box; | ||||
|   height: auto; | ||||
|   padding: 10px; | ||||
|   font-size: 16px; | ||||
| } | ||||
| .form-signin .form-control:focus { | ||||
|   z-index: 2; | ||||
| } | ||||
| .form-signin input[type="text"] { | ||||
|   margin-bottom: -1px; | ||||
|   border-bottom-right-radius: 0; | ||||
|   border-bottom-left-radius: 0; | ||||
| } | ||||
| .form-signin input[type="password"] { | ||||
|   margin-bottom: 10px; | ||||
|   border-top-left-radius: 0; | ||||
|   border-top-right-radius: 0; | ||||
| } | ||||
							
								
								
									
										17
									
								
								src-ui/src/app/components/login/login.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src-ui/src/app/components/login/login.component.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| <div class="form-signin-container"> | ||||
|   <form class="form-signin mt-5" [formGroup]="loginForm" (ngSubmit)="loginClicked()"> | ||||
|     <img class="mb-4" src="assets/logo.svg" alt="" width="100%"> | ||||
|     <h1 class="h3 mb-3 font-weight-normal">Login</h1> | ||||
|     <label for="inputUsername" class="sr-only">Username</label> | ||||
|     <input type="text" id="inputUsername" class="form-control" placeholder="Username" required autofocus formControlName="username"> | ||||
|     <label for="inputPassword" class="sr-only">Password</label> | ||||
|     <input type="password" id="inputPassword" class="form-control" placeholder="Password" required formControlName="password"> | ||||
|     <div class="checkbox mb-3"> | ||||
|       <label> | ||||
|         <input type="checkbox" value="remember-me" formControlName="rememberMe"> Remember me | ||||
|       </label> | ||||
|     </div> | ||||
|     <button class="btn btn-lg btn-primary btn-block mb-4" type="submit">Login</button> | ||||
|     <p><a href="/admin/">Go to admin interface</a></p> | ||||
|   </form> | ||||
| </div> | ||||
							
								
								
									
										25
									
								
								src-ui/src/app/components/login/login.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src-ui/src/app/components/login/login.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { LoginComponent } from './login.component'; | ||||
|  | ||||
| describe('LoginComponent', () => { | ||||
|   let component: LoginComponent; | ||||
|   let fixture: ComponentFixture<LoginComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ LoginComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(LoginComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										34
									
								
								src-ui/src/app/components/login/login.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src-ui/src/app/components/login/login.component.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import { FormControl, FormGroup } from '@angular/forms'; | ||||
| import { Router } from '@angular/router'; | ||||
| import { AuthService } from 'src/app/services/auth.service'; | ||||
| import { Toast, ToastService } from 'src/app/services/toast.service'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-login', | ||||
|   templateUrl: './login.component.html', | ||||
|   styleUrls: ['./login.component.css'] | ||||
| }) | ||||
| export class LoginComponent implements OnInit { | ||||
|  | ||||
|   constructor(private auth: AuthService, private router: Router, private toastService: ToastService) { } | ||||
|  | ||||
|   loginForm = new FormGroup({ | ||||
|     username: new FormControl(''), | ||||
|     password: new FormControl(''), | ||||
|     rememberMe: new FormControl(false) | ||||
|   }) | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|   } | ||||
|  | ||||
|   loginClicked() { | ||||
|     this.auth.login(this.loginForm.value.username, this.loginForm.value.password, this.loginForm.value.rememberMe).subscribe(result => { | ||||
|       this.router.navigate(['']) | ||||
|     }, (error) => { | ||||
|       this.toastService.showToast(Toast.make("Error", "result: " + JSON.stringify(error.error))) | ||||
|     } | ||||
|     ) | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,23 @@ | ||||
| <form [formGroup]="objectForm" class="needs-validation" novalidate (ngSubmit)="save()"> | ||||
|   <div class="modal-header"> | ||||
|     <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4> | ||||
|     <button type="button" class="close" aria-label="Close" (click)="cancel()"> | ||||
|       <span aria-hidden="true">×</span> | ||||
|     </button> | ||||
|   </div> | ||||
|   <div class="modal-body"> | ||||
|     <div class="form-group"> | ||||
|       <label for="name">Name</label> | ||||
|       <input id="name" class="form-control" formControlName="name" required ngbAutofocus> | ||||
|     </div> | ||||
|     <div class="form-group form-check"> | ||||
|       <input type="checkbox" class="form-check-input" id="automatic_classification" | ||||
|         formControlName="automatic_classification"> | ||||
|       <label class="form-check-label" for="automatic_classification">Automatic Classification</label> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="modal-footer"> | ||||
|     <button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button> | ||||
|     <button type="submit" class="btn btn-primary">Save</button> | ||||
|   </div> | ||||
| </form> | ||||
| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog.component'; | ||||
|  | ||||
| describe('CorrespondentEditDialogComponent', () => { | ||||
|   let component: CorrespondentEditDialogComponent; | ||||
|   let fixture: ComponentFixture<CorrespondentEditDialogComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ CorrespondentEditDialogComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(CorrespondentEditDialogComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,27 @@ | ||||
| import { Component, EventEmitter, Input, OnInit, Output } 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 { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'; | ||||
| import { CorrespondentService } from 'src/app/services/rest/correspondent.service'; | ||||
| import { ToastService } from 'src/app/services/toast.service'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-correspondent-edit-dialog', | ||||
|   templateUrl: './correspondent-edit-dialog.component.html', | ||||
|   styleUrls: ['./correspondent-edit-dialog.component.css'] | ||||
| }) | ||||
| export class CorrespondentEditDialogComponent extends EditDialogComponent<PaperlessCorrespondent> { | ||||
|  | ||||
|   constructor(service: CorrespondentService, activeModal: NgbActiveModal, toastService: ToastService) { | ||||
|     super(service, activeModal, toastService, 'correspondent') | ||||
|   } | ||||
|  | ||||
|   getForm(): FormGroup { | ||||
|     return new FormGroup({ | ||||
|       name: new FormControl(''), | ||||
|       automatic_classification: new FormControl(true) | ||||
|     }) | ||||
|   }   | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,33 @@ | ||||
| <app-page-header title="Correspondents"> | ||||
|   <button type="button" class="btn btn-sm btn-outline-secondary" (click)="openCreateDialog()"> | ||||
|     Create | ||||
|   </button> | ||||
| </app-page-header> | ||||
|  | ||||
| <ngb-pagination [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" (pageChange)="reloadData()" aria-label="Default pagination"></ngb-pagination> | ||||
|  | ||||
| <table class="table table-striped"> | ||||
|     <thead> | ||||
|     <tr> | ||||
|       <th scope="col">Name</th> | ||||
|       <th scope="col">Automatic Classification</th> | ||||
|       <th scope="col">Document count</th> | ||||
|       <th scope="col">Last correspondence</th> | ||||
|       <th scope="col">Actions</th> | ||||
|     </tr> | ||||
|     </thead> | ||||
|     <tbody> | ||||
|     <tr *ngFor="let correspondent of data"> | ||||
|       <td scope="row">{{ correspondent.name }}</td> | ||||
|       <td scope="row">{{ correspondent.automatic_classification }}</td> | ||||
|       <td scope="row">{{ correspondent.document_count }}</td> | ||||
|       <td scope="row">{{ correspondent.last_correspondence | date }}</td> | ||||
|         <td scope="row"> | ||||
|           <div class="btn-group"> | ||||
|           <button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(correspondent)">Edit</button> | ||||
|           <button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(correspondent)">Delete</button> | ||||
|         </div> | ||||
|         </td> | ||||
|     </tr> | ||||
|     </tbody> | ||||
|   </table> | ||||
| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { CorrespondentListComponent } from './correspondent-list.component'; | ||||
|  | ||||
| describe('CorrespondentListComponent', () => { | ||||
|   let component: CorrespondentListComponent; | ||||
|   let fixture: ComponentFixture<CorrespondentListComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ CorrespondentListComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(CorrespondentListComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,20 @@ | ||||
| import { Component } from '@angular/core'; | ||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; | ||||
| import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'; | ||||
| import { CorrespondentService } from 'src/app/services/rest/correspondent.service'; | ||||
| import { GenericListComponent } from '../generic-list/generic-list.component'; | ||||
| import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog/correspondent-edit-dialog.component'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-correspondent-list', | ||||
|   templateUrl: './correspondent-list.component.html', | ||||
|   styleUrls: ['./correspondent-list.component.css'] | ||||
| }) | ||||
| export class CorrespondentListComponent extends GenericListComponent<PaperlessCorrespondent> { | ||||
|  | ||||
|   constructor(correspondentsService: CorrespondentService, | ||||
|     modalService: NgbModal) {  | ||||
|       super(correspondentsService,modalService,CorrespondentEditDialogComponent) | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,23 @@ | ||||
| <form [formGroup]="objectForm" class="needs-validation" novalidate (ngSubmit)="save()"> | ||||
|     <div class="modal-header"> | ||||
|       <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4> | ||||
|       <button type="button" class="close" aria-label="Close" (click)="cancel()"> | ||||
|         <span aria-hidden="true">×</span> | ||||
|       </button> | ||||
|     </div> | ||||
|     <div class="modal-body"> | ||||
|       <div class="form-group"> | ||||
|         <label for="name">Name</label> | ||||
|         <input id="name" class="form-control" formControlName="name" required ngbAutofocus> | ||||
|       </div> | ||||
|       <div class="form-group form-check"> | ||||
|         <input type="checkbox" class="form-check-input" id="automatic_classification" | ||||
|           formControlName="automatic_classification"> | ||||
|         <label class="form-check-label" for="automatic_classification">Automatic Classification</label> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="modal-footer"> | ||||
|       <button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button> | ||||
|       <button type="submit" class="btn btn-primary">Save</button> | ||||
|     </div> | ||||
|   </form> | ||||
| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog.component'; | ||||
|  | ||||
| describe('DocumentTypeEditDialogComponent', () => { | ||||
|   let component: DocumentTypeEditDialogComponent; | ||||
|   let fixture: ComponentFixture<DocumentTypeEditDialogComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ DocumentTypeEditDialogComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(DocumentTypeEditDialogComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,27 @@ | ||||
| import { Component, OnInit } 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 { 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'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-document-type-edit-dialog', | ||||
|   templateUrl: './document-type-edit-dialog.component.html', | ||||
|   styleUrls: ['./document-type-edit-dialog.component.css'] | ||||
| }) | ||||
| export class DocumentTypeEditDialogComponent extends EditDialogComponent<PaperlessDocumentType> { | ||||
|  | ||||
|   constructor(service: DocumentTypeService, activeModal: NgbActiveModal, toastService: ToastService) {  | ||||
|     super(service, activeModal, toastService, 'document type') | ||||
|   } | ||||
|  | ||||
|   getForm(): FormGroup { | ||||
|     return new FormGroup({ | ||||
|       name: new FormControl(''), | ||||
|       automatic_classification: new FormControl(true) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,32 @@ | ||||
| <app-page-header title="Document types"> | ||||
|   <button type="button" class="btn btn-sm btn-outline-secondary" (click)="openCreateDialog()"> | ||||
|     Create | ||||
|   </button> | ||||
| </app-page-header> | ||||
|  | ||||
| <ngb-pagination [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" (pageChange)="reloadData()" | ||||
|   aria-label="Default pagination"></ngb-pagination> | ||||
|  | ||||
| <table class="table table-striped"> | ||||
|   <thead> | ||||
|     <tr> | ||||
|       <th scope="col">Name</th> | ||||
|       <th scope="col">Automatic Classification</th> | ||||
|       <th scope="col">Document count</th> | ||||
|       <th scope="col">Actions</th> | ||||
|     </tr> | ||||
|   </thead> | ||||
|   <tbody> | ||||
|     <tr *ngFor="let correspondent of data"> | ||||
|       <td scope="row">{{ correspondent.name }}</td> | ||||
|       <td scope="row">{{ correspondent.automatic_classification }}</td> | ||||
|       <td scope="row">{{ correspondent.document_count }}</td> | ||||
|       <td scope="row"> | ||||
|         <div class="btn-group"> | ||||
|           <button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(correspondent)">Edit</button> | ||||
|           <button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(correspondent)">Delete</button> | ||||
|         </div> | ||||
|       </td> | ||||
|     </tr> | ||||
|   </tbody> | ||||
| </table> | ||||
| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { DocumentTypeListComponent } from './document-type-list.component'; | ||||
|  | ||||
| describe('DocumentTypeListComponent', () => { | ||||
|   let component: DocumentTypeListComponent; | ||||
|   let fixture: ComponentFixture<DocumentTypeListComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ DocumentTypeListComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(DocumentTypeListComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,19 @@ | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; | ||||
| import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'; | ||||
| import { DocumentTypeService } from 'src/app/services/rest/document-type.service'; | ||||
| import { GenericListComponent } from '../generic-list/generic-list.component'; | ||||
| import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog/document-type-edit-dialog.component'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-document-type-list', | ||||
|   templateUrl: './document-type-list.component.html', | ||||
|   styleUrls: ['./document-type-list.component.css'] | ||||
| }) | ||||
| export class DocumentTypeListComponent extends GenericListComponent<PaperlessDocumentType> { | ||||
|  | ||||
|   constructor(service: DocumentTypeService, modalService: NgbModal) { | ||||
|     super(service, modalService, DocumentTypeEditDialogComponent) | ||||
|    } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { GenericListComponent } from './generic-list.component'; | ||||
|  | ||||
| describe('GenericListComponent', () => { | ||||
|   let component: GenericListComponent; | ||||
|   let fixture: ComponentFixture<GenericListComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ GenericListComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(GenericListComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,62 @@ | ||||
| import { Directive, OnInit } from '@angular/core'; | ||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; | ||||
| import { ObjectWithId } from 'src/app/data/object-with-id'; | ||||
| import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'; | ||||
| import { DeleteDialogComponent } from '../../common/delete-dialog/delete-dialog.component'; | ||||
|  | ||||
| @Directive() | ||||
| export abstract class GenericListComponent<T extends ObjectWithId> implements OnInit { | ||||
|    | ||||
|   constructor( | ||||
|     private service: AbstractPaperlessService<T>,  | ||||
|     private modalService: NgbModal, | ||||
|     private editDialogComponent: any) { | ||||
|     } | ||||
|  | ||||
|   public data: T[] = [] | ||||
|  | ||||
|   public page = 1 | ||||
|  | ||||
|   public collectionSize = 0 | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|     this.reloadData() | ||||
|   } | ||||
|  | ||||
|   reloadData() { | ||||
|     this.service.list(this.page).subscribe(c => { | ||||
|       this.data = c.results | ||||
|       this.collectionSize = c.count | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   openCreateDialog() { | ||||
|     var activeModal = this.modalService.open(this.editDialogComponent, {backdrop: 'static'}) | ||||
|     activeModal.componentInstance.dialogMode = 'create' | ||||
|     activeModal.componentInstance.success.subscribe(o => { | ||||
|       this.reloadData() | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   openEditDialog(object: T) { | ||||
|     var activeModal = this.modalService.open(this.editDialogComponent, {backdrop: 'static'}) | ||||
|     activeModal.componentInstance.object = object | ||||
|     activeModal.componentInstance.dialogMode = 'edit' | ||||
|     activeModal.componentInstance.success.subscribe(o => { | ||||
|       this.reloadData() | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   openDeleteDialog(object: T) { | ||||
|     var activeModal = this.modalService.open(DeleteDialogComponent, {backdrop: 'static'}) | ||||
|     activeModal.componentInstance.message = `Do you really want to delete ${object}?` | ||||
|     activeModal.componentInstance.message2 = "Associated documents will not be deleted." | ||||
|     activeModal.componentInstance.deleteClicked.subscribe(() => { | ||||
|       this.service.delete(object).subscribe(_ => { | ||||
|         activeModal.close() | ||||
|         this.reloadData() | ||||
|       }) | ||||
|     } | ||||
|     ) | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,2 @@ | ||||
| <app-page-header title="Logs"> | ||||
| </app-page-header> | ||||
							
								
								
									
										25
									
								
								src-ui/src/app/components/manage/logs/logs.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src-ui/src/app/components/manage/logs/logs.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { LogsComponent } from './logs.component'; | ||||
|  | ||||
| describe('LogsComponent', () => { | ||||
|   let component: LogsComponent; | ||||
|   let fixture: ComponentFixture<LogsComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ LogsComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(LogsComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										15
									
								
								src-ui/src/app/components/manage/logs/logs.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src-ui/src/app/components/manage/logs/logs.component.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-logs', | ||||
|   templateUrl: './logs.component.html', | ||||
|   styleUrls: ['./logs.component.css'] | ||||
| }) | ||||
| export class LogsComponent implements OnInit { | ||||
|  | ||||
|   constructor() { } | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,6 @@ | ||||
| <app-page-header title="Settings"> | ||||
|  | ||||
| </app-page-header> | ||||
|  | ||||
| <p>items per page, documents per view type</p> | ||||
|  | ||||
| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { SettingsComponent } from './settings.component'; | ||||
|  | ||||
| describe('SettingsComponent', () => { | ||||
|   let component: SettingsComponent; | ||||
|   let fixture: ComponentFixture<SettingsComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ SettingsComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(SettingsComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,15 @@ | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-settings', | ||||
|   templateUrl: './settings.component.html', | ||||
|   styleUrls: ['./settings.component.css'] | ||||
| }) | ||||
| export class SettingsComponent implements OnInit { | ||||
|  | ||||
|   constructor() { } | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,34 @@ | ||||
|   <form [formGroup]="objectForm" class="needs-validation" novalidate (ngSubmit)="save()"> | ||||
|     <div class="modal-header"> | ||||
|       <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4> | ||||
|       <button type="button" class="close" aria-label="Close" (click)="cancel()"> | ||||
|         <span aria-hidden="true">×</span> | ||||
|       </button> | ||||
|     </div> | ||||
|     <div class="modal-body"> | ||||
|       <div class="form-group"> | ||||
|         <label for="name">Name</label> | ||||
|         <input id="name" class="form-control" formControlName="name" required ngbAutofocus> | ||||
|       </div> | ||||
|       <div class="form-group"> | ||||
|         <label for="colour">Colour</label> | ||||
|         <select id="colour" class="form-control" required formControlName="colour" [style.color]="getColor(objectForm.value.colour).textColor" [style.background]="getColor(objectForm.value.colour).value"> | ||||
|           <option *ngFor="let colour of getColours()" [ngValue]="colour.id" class="form-control">{{colour.name}}</option> | ||||
|         </select> | ||||
|       </div> | ||||
|       <div class="form-group form-check"> | ||||
|         <input type="checkbox" class="form-check-input" id="is_inbox_tag" | ||||
|           formControlName="is_inbox_tag"> | ||||
|         <label class="form-check-label" for="is_inbox_tag">Inbox Tag</label> | ||||
|       </div> | ||||
|       <div class="form-group form-check"> | ||||
|         <input type="checkbox" class="form-check-input" id="automatic_classification" | ||||
|           formControlName="automatic_classification"> | ||||
|         <label class="form-check-label" for="automatic_classification">Automatic Classification</label> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="modal-footer"> | ||||
|       <button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button> | ||||
|       <button type="submit" class="btn btn-primary">Save</button> | ||||
|     </div> | ||||
|   </form> | ||||
| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { TagEditDialogComponent } from './tag-edit-dialog.component'; | ||||
|  | ||||
| describe('TagEditDialogComponent', () => { | ||||
|   let component: TagEditDialogComponent; | ||||
|   let fixture: ComponentFixture<TagEditDialogComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ TagEditDialogComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(TagEditDialogComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,37 @@ | ||||
| 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 { PaperlessTag } from 'src/app/data/paperless-tag'; | ||||
| import { TagService } from 'src/app/services/rest/tag.service'; | ||||
| import { ToastService } from 'src/app/services/toast.service'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-tag-edit-dialog', | ||||
|   templateUrl: './tag-edit-dialog.component.html', | ||||
|   styleUrls: ['./tag-edit-dialog.component.css'] | ||||
| }) | ||||
| export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> { | ||||
|  | ||||
|   constructor(service: TagService, activeModal: NgbActiveModal, toastService: ToastService) {  | ||||
|     super(service, activeModal, toastService, 'tag') | ||||
|   } | ||||
|  | ||||
|   getForm(): FormGroup { | ||||
|     return new FormGroup({ | ||||
|       name: new FormControl(''), | ||||
|       colour: new FormControl(1), | ||||
|       is_inbox_tag: new FormControl(false), | ||||
|       automatic_classification: new FormControl(true) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   getColours() { | ||||
|     return PaperlessTag.COLOURS | ||||
|   } | ||||
|  | ||||
|   getColor(id: number) { | ||||
|     return PaperlessTag.COLOURS.find(c => c.id == id) | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,34 @@ | ||||
| <app-page-header title="Tags"> | ||||
|   <button type="button" class="btn btn-sm btn-outline-secondary" (click)="openCreateDialog()"> | ||||
|     Create | ||||
|   </button> | ||||
| </app-page-header> | ||||
|    | ||||
| <ngb-pagination [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" (pageChange)="reloadData()" aria-label="Default pagination"></ngb-pagination> | ||||
|    | ||||
|   <table class="table table-striped"> | ||||
|       <thead> | ||||
|       <tr> | ||||
|         <th scope="col">Name</th> | ||||
|         <th scope="col">Colour</th> | ||||
|         <th scope="col">Automatic Classification</th> | ||||
|         <th scope="col">Document count</th> | ||||
|         <th scope="col">Actions</th> | ||||
|       </tr> | ||||
|       </thead> | ||||
|       <tbody> | ||||
|       <tr *ngFor="let tag of data"> | ||||
|           <td scope="row">{{ tag.name }}</td> | ||||
|           <td scope="row"><span class="badge" [style.color]="getColor(tag.colour).textColor" [style.background-color]="getColor(tag.colour).value">{{ getColor(tag.colour).name }}</span></td> | ||||
|           <td scope="row">{{ tag.automatic_classification }}</td> | ||||
|           <td scope="row">{{ tag.document_count }}</td> | ||||
|           <td scope="row"> | ||||
|             <div class="btn-group"> | ||||
|             <button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(tag)">Edit</button> | ||||
|             <button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(tag)">Delete</button> | ||||
|           </div> | ||||
|           </td> | ||||
|       </tr> | ||||
|       </tbody> | ||||
|     </table> | ||||
|    | ||||
| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { TagListComponent } from './tag-list.component'; | ||||
|  | ||||
| describe('TagListComponent', () => { | ||||
|   let component: TagListComponent; | ||||
|   let fixture: ComponentFixture<TagListComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ TagListComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(TagListComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,23 @@ | ||||
| import { Component } from '@angular/core'; | ||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; | ||||
| import { PaperlessTag } from 'src/app/data/paperless-tag'; | ||||
| import { TagService } from 'src/app/services/rest/tag.service'; | ||||
| import { CorrespondentEditDialogComponent } from '../correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component'; | ||||
| import { GenericListComponent } from '../generic-list/generic-list.component'; | ||||
| import { TagEditDialogComponent } from './tag-edit-dialog/tag-edit-dialog.component'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-tag-list', | ||||
|   templateUrl: './tag-list.component.html', | ||||
|   styleUrls: ['./tag-list.component.css'] | ||||
| }) | ||||
| export class TagListComponent extends GenericListComponent<PaperlessTag> { | ||||
|  | ||||
|   constructor(tagService: TagService, modalService: NgbModal) { | ||||
|     super(tagService, modalService, TagEditDialogComponent) | ||||
|    } | ||||
|  | ||||
|   getColor(id) { | ||||
|     return PaperlessTag.COLOURS.find(c => c.id == id) | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,8 @@ | ||||
| <div class="jumbotron text-center"> | ||||
|     <svg width="5em" height="7em" viewBox="0 0 16 16" class="bi bi-emoji-frown" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | ||||
|         <path fill-rule="evenodd" d="M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/> | ||||
|         <path fill-rule="evenodd" d="M4.285 12.433a.5.5 0 0 0 .683-.183A3.498 3.498 0 0 1 8 10.5c1.295 0 2.426.703 3.032 1.75a.5.5 0 0 0 .866-.5A4.498 4.498 0 0 0 8 9.5a4.5 4.5 0 0 0-3.898 2.25.5.5 0 0 0 .183.683z"/> | ||||
|         <path d="M7 6.5C7 7.328 6.552 8 6 8s-1-.672-1-1.5S5.448 5 6 5s1 .672 1 1.5zm4 0c0 .828-.448 1.5-1 1.5s-1-.672-1-1.5S9.448 5 10 5s1 .672 1 1.5z"/> | ||||
|       </svg> | ||||
|     <h1>404 Not Found</h1> | ||||
| </div> | ||||
| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { NotFoundComponent } from './not-found.component'; | ||||
|  | ||||
| describe('NotFoundComponent', () => { | ||||
|   let component: NotFoundComponent; | ||||
|   let fixture: ComponentFixture<NotFoundComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ NotFoundComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(NotFoundComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										15
									
								
								src-ui/src/app/components/not-found/not-found.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src-ui/src/app/components/not-found/not-found.component.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-not-found', | ||||
|   templateUrl: './not-found.component.html', | ||||
|   styleUrls: ['./not-found.component.css'] | ||||
| }) | ||||
| export class NotFoundComponent implements OnInit { | ||||
|  | ||||
|   constructor() { } | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,4 @@ | ||||
| .match { | ||||
|     color: black; | ||||
|     background-color: orange; | ||||
| } | ||||
| @@ -0,0 +1,3 @@ | ||||
| ... <span *ngFor="let fragment of highlights"> | ||||
|     <span *ngFor="let token of fragment" [ngClass]="token.term != null ? 'match term'+ token.term : ''">{{token.text}}</span> ...  | ||||
| </span> | ||||
| @@ -0,0 +1,25 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { ResultHightlightComponent } from './result-hightlight.component'; | ||||
|  | ||||
| describe('ResultHightlightComponent', () => { | ||||
|   let component: ResultHightlightComponent; | ||||
|   let fixture: ComponentFixture<ResultHightlightComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ ResultHightlightComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(ResultHightlightComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,19 @@ | ||||
| import { Component, Input, OnInit } from '@angular/core'; | ||||
| import { SearchResultHighlightedText } from 'src/app/services/rest/search.service'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-result-hightlight', | ||||
|   templateUrl: './result-hightlight.component.html', | ||||
|   styleUrls: ['./result-hightlight.component.css'] | ||||
| }) | ||||
| export class ResultHightlightComponent implements OnInit { | ||||
|  | ||||
|   constructor() { } | ||||
|  | ||||
|   @Input() | ||||
|   highlights: SearchResultHighlightedText[][] | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										11
									
								
								src-ui/src/app/components/search/search.component.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src-ui/src/app/components/search/search.component.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| .result-content { | ||||
|     color: darkgray; | ||||
| } | ||||
|  | ||||
| .doc-img { | ||||
|     object-fit: cover; | ||||
|     object-position: top; | ||||
|     height: 100%; | ||||
|     position: absolute; | ||||
|  | ||||
| } | ||||
							
								
								
									
										12
									
								
								src-ui/src/app/components/search/search.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src-ui/src/app/components/search/search.component.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| <app-page-header title="Search results"> | ||||
| </app-page-header> | ||||
|  | ||||
| <p>Search string: <i>{{query}}</i></p> | ||||
|  | ||||
| <app-document-card-large *ngFor="let result of results" | ||||
|     [document]="result.document" | ||||
|     [details]="result.highlights"> | ||||
|  | ||||
| </app-document-card-large> | ||||
|  | ||||
| <p *ngIf="results.length == 0" class="mx-auto">No results</p> | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	 Jonas Winkler
					Jonas Winkler