mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-03-31 13:35:08 -05:00
added paperless ui
This commit is contained in:
parent
d5a1a4916e
commit
131533d9eb
src-ui
.browserslistrc.editorconfig.gitignoreREADME.mdangular.json
e2e
karma.conf.jspackage-lock.jsonpackage.jsonsrc/app
app-routing.module.tsapp.component.cssapp.component.htmlapp.component.spec.tsapp.component.tsapp.module.ts
components
app-frame
common
delete-dialog
delete-dialog.component.cssdelete-dialog.component.htmldelete-dialog.component.spec.tsdelete-dialog.component.ts
edit-dialog
page-header
page-header.component.csspage-header.component.htmlpage-header.component.spec.tspage-header.component.ts
tag
toasts
dashboard
document-detail
document-detail.component.cssdocument-detail.component.htmldocument-detail.component.spec.tsdocument-detail.component.ts
document-list
document-card-large
document-card-large.component.cssdocument-card-large.component.htmldocument-card-large.component.spec.tsdocument-card-large.component.ts
document-card-small
document-card-small.component.cssdocument-card-small.component.htmldocument-card-small.component.spec.tsdocument-card-small.component.ts
document-list.component.cssdocument-list.component.htmldocument-list.component.spec.tsdocument-list.component.tsfilter-editor
filter-editor.component.cssfilter-editor.component.htmlfilter-editor.component.spec.tsfilter-editor.component.ts
login
manage
correspondent-list
correspondent-edit-dialog
correspondent-edit-dialog.component.csscorrespondent-edit-dialog.component.htmlcorrespondent-edit-dialog.component.spec.tscorrespondent-edit-dialog.component.ts
correspondent-list.component.csscorrespondent-list.component.htmlcorrespondent-list.component.spec.tscorrespondent-list.component.tsdocument-type-list
document-type-edit-dialog
document-type-edit-dialog.component.cssdocument-type-edit-dialog.component.htmldocument-type-edit-dialog.component.spec.tsdocument-type-edit-dialog.component.ts
document-type-list.component.cssdocument-type-list.component.htmldocument-type-list.component.spec.tsdocument-type-list.component.tsgeneric-list
logs
settings
tag-list
18
src-ui/.browserslistrc
Normal file
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
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
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
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
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"
|
||||
}
|
36
src-ui/e2e/protractor.conf.js
Normal file
36
src-ui/e2e/protractor.conf.js
Normal file
@ -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
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
23
src-ui/e2e/src/app.e2e-spec.ts
Normal file
23
src-ui/e2e/src/app.e2e-spec.ts
Normal file
@ -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
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
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
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
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
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"
|
||||
}
|
||||
}
|
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 {
|
||||
}
|
||||
|
||||
}
|
0
src-ui/src/app/components/common/tag/tag.component.css
Normal file
0
src-ui/src/app/components/common/tag/tag.component.css
Normal file
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)
|
||||
}
|
||||
}
|
11
src-ui/src/app/components/document-list/document-card-large/document-card-large.component.css
Normal file
11
src-ui/src/app/components/document-list/document-card-large/document-card-large.component.css
Normal file
@ -0,0 +1,11 @@
|
||||
.result-content {
|
||||
color: darkgray;
|
||||
}
|
||||
|
||||
.doc-img {
|
||||
object-fit: cover;
|
||||
object-position: top;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
|
||||
}
|
38
src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html
Normal file
38
src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html
Normal file
@ -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>
|
25
src-ui/src/app/components/document-list/document-card-large/document-card-large.component.spec.ts
Normal file
25
src-ui/src/app/components/document-list/document-card-large/document-card-large.component.spec.ts
Normal file
@ -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();
|
||||
});
|
||||
});
|
46
src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts
Normal file
46
src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts
Normal file
@ -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)
|
||||
}
|
||||
}
|
5
src-ui/src/app/components/document-list/document-card-small/document-card-small.component.css
Normal file
5
src-ui/src/app/components/document-list/document-card-small/document-card-small.component.css
Normal file
@ -0,0 +1,5 @@
|
||||
.doc-img {
|
||||
object-fit: cover;
|
||||
object-position: top;
|
||||
|
||||
}
|
26
src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html
Normal file
26
src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html
Normal file
@ -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>
|
25
src-ui/src/app/components/document-list/document-card-small/document-card-small.component.spec.ts
Normal file
25
src-ui/src/app/components/document-list/document-card-small/document-card-small.component.spec.ts
Normal file
@ -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();
|
||||
});
|
||||
});
|
27
src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts
Normal file
27
src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts
Normal file
@ -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
src-ui/src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.css
Normal file
0
src-ui/src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.css
Normal file
23
src-ui/src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.html
Normal file
23
src-ui/src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.html
Normal file
@ -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>
|
25
src-ui/src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.spec.ts
Normal file
25
src-ui/src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.spec.ts
Normal file
@ -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();
|
||||
});
|
||||
});
|
27
src-ui/src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.ts
Normal file
27
src-ui/src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.ts
Normal file
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
33
src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.html
Normal file
33
src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.html
Normal file
@ -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>
|
25
src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.spec.ts
Normal file
25
src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.spec.ts
Normal file
@ -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
src-ui/src/app/components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component.css
Normal file
0
src-ui/src/app/components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component.css
Normal file
23
src-ui/src/app/components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component.html
Normal file
23
src-ui/src/app/components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component.html
Normal file
@ -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>
|
25
src-ui/src/app/components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component.spec.ts
Normal file
25
src-ui/src/app/components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component.spec.ts
Normal file
@ -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();
|
||||
});
|
||||
});
|
27
src-ui/src/app/components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component.ts
Normal file
27
src-ui/src/app/components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component.ts
Normal file
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
32
src-ui/src/app/components/manage/document-type-list/document-type-list.component.html
Normal file
32
src-ui/src/app/components/manage/document-type-list/document-type-list.component.html
Normal file
@ -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>
|
25
src-ui/src/app/components/manage/document-type-list/document-type-list.component.spec.ts
Normal file
25
src-ui/src/app/components/manage/document-type-list/document-type-list.component.spec.ts
Normal file
@ -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
src-ui/src/app/components/manage/logs/logs.component.css
Normal file
0
src-ui/src/app/components/manage/logs/logs.component.css
Normal file
@ -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
src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.css
Normal file
0
src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.css
Normal file
34
src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.html
Normal file
34
src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.html
Normal file
@ -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>
|
25
src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.spec.ts
Normal file
25
src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.spec.ts
Normal file
@ -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();
|
||||
});
|
||||
});
|
37
src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.ts
Normal file
37
src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.ts
Normal file
@ -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
Loading…
x
Reference in New Issue
Block a user