added paperless ui

This commit is contained in:
Jonas Winkler 2020-10-27 01:10:18 +01:00
parent d5a1a4916e
commit 131533d9eb
173 changed files with 18693 additions and 0 deletions
src-ui
.browserslistrc.editorconfig.gitignoreREADME.mdangular.json
e2e
karma.conf.jspackage-lock.jsonpackage.json
src/app
app-routing.module.tsapp.component.cssapp.component.htmlapp.component.spec.tsapp.component.tsapp.module.ts
components
app-frame
common
dashboard
document-detail
document-list
filter-editor
login
manage

18
src-ui/.browserslistrc Normal file

@ -0,0 +1,18 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line.
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.

16
src-ui/.editorconfig Normal file

@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

46
src-ui/.gitignore vendored Normal file

@ -0,0 +1,46 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
# profiling files
chrome-profiler-events*.json
speed-measure-plugin*.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db

27
src-ui/README.md Normal file

@ -0,0 +1,27 @@
# PaperlessUi
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 10.1.5.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).

125
src-ui/angular.json Normal file

@ -0,0 +1,125 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"paperless-ui": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/paperless-ui",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": true,
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"src/styles.css"
],
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "paperless-ui:build"
},
"configurations": {
"production": {
"browserTarget": "paperless-ui:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "paperless-ui:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.css"
],
"scripts": []
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "paperless-ui:serve"
},
"configurations": {
"production": {
"devServerTarget": "paperless-ui:serve:production"
}
}
}
}
}},
"defaultProject": "paperless-ui"
}

@ -0,0 +1,36 @@
// @ts-check
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter');
/**
* @type { import("protractor").Config }
*/
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./src/**/*.e2e-spec.ts'
],
capabilities: {
browserName: 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.json')
});
jasmine.getEnv().addReporter(new SpecReporter({
spec: {
displayStacktrace: StacktraceOption.PRETTY
}
}));
}
};

@ -0,0 +1,23 @@
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';
describe('workspace-project App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getTitleText()).toEqual('paperless-ui app is running!');
});
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
});
});

11
src-ui/e2e/src/app.po.ts Normal file

@ -0,0 +1,11 @@
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo(): Promise<unknown> {
return browser.get(browser.baseUrl) as Promise<unknown>;
}
getTitleText(): Promise<string> {
return element(by.css('app-root .content span')).getText() as Promise<string>;
}
}

14
src-ui/e2e/tsconfig.json Normal file

@ -0,0 +1,14 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es2018",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

32
src-ui/karma.conf.js Normal file

@ -0,0 +1,32 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage/paperless-ui'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

14203
src-ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

50
src-ui/package.json Normal file

@ -0,0 +1,50 @@
{
"name": "paperless-ui",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "~10.1.5",
"@angular/common": "~10.1.5",
"@angular/compiler": "~10.1.5",
"@angular/core": "~10.1.5",
"@angular/forms": "~10.1.5",
"@angular/localize": "~10.1.5",
"@angular/platform-browser": "~10.1.5",
"@angular/platform-browser-dynamic": "~10.1.5",
"@angular/router": "~10.1.5",
"@ng-bootstrap/ng-bootstrap": "^7.0.0",
"bootstrap": "^4.5.0",
"ng-bootstrap": "^1.6.3",
"rxjs": "~6.6.0",
"tslib": "^2.0.0",
"zone.js": "~0.10.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "^0.1002.0",
"@angular/cli": "~10.1.5",
"@angular/compiler-cli": "~10.1.5",
"@types/jasmine": "~3.5.0",
"@types/jasminewd2": "~2.0.3",
"@types/node": "^12.11.1",
"codelyzer": "^6.0.0",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~5.0.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"protractor": "~7.0.0",
"ts-node": "~8.3.0",
"tslint": "~6.1.0",
"typescript": "~4.0.2"
}
}

@ -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,0 +1,3 @@
<app-toasts></app-toasts>
<router-outlet></router-outlet>

@ -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!');
});
});

@ -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 () {
}
}

@ -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 { }

@ -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;
}

@ -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();
});
});

@ -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">&times;</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 {
}
}

@ -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>

@ -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();
});
});

@ -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();
});
});

@ -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)
}
}

@ -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();
});
});

@ -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>
&nbsp;
</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>&nbsp;
<button type="submit" class="btn btn-primary">Save</button>&nbsp;
</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
}
}

@ -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;
}

@ -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>

@ -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();
});
});

@ -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">&times;</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">&times;</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>

@ -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();
});
});

@ -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">&times;</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>

Some files were not shown because too many files have changed in this diff Show More