Fix line endings
This commit is contained in:
parent
f18ea6cf34
commit
a5f9beb905
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
|
51
src-ui/.eslintrc.json
Normal file
51
src-ui/.eslintrc.json
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"ignorePatterns": [
|
||||||
|
"projects/**/*"
|
||||||
|
],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"*.ts"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"project": [
|
||||||
|
"tsconfig.json",
|
||||||
|
"e2e/tsconfig.json"
|
||||||
|
],
|
||||||
|
"createDefaultProgram": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"plugin:@angular-eslint/recommended",
|
||||||
|
"plugin:@angular-eslint/template/process-inline-templates"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"@angular-eslint/directive-selector": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"type": "attribute",
|
||||||
|
"prefix": "app",
|
||||||
|
"style": "camelCase"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@angular-eslint/component-selector": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"type": "element",
|
||||||
|
"prefix": "app",
|
||||||
|
"style": "kebab-case"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"*.html"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"plugin:@angular-eslint/template/recommended"
|
||||||
|
],
|
||||||
|
"rules": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
51
src-ui/.gitignore
vendored
Normal file
51
src-ui/.gitignore
vendored
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# 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
|
||||||
|
/.angular/cache
|
||||||
|
/.sass-cache
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
|
||||||
|
# System Files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Cypress
|
||||||
|
cypress/videos/**/*
|
||||||
|
cypress/screenshots/**/*
|
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 `--configuration production` 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).
|
206
src-ui/angular.json
Normal file
206
src-ui/angular.json
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"paperless-ui": {
|
||||||
|
"projectType": "application",
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"style": "scss"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"i18n": {
|
||||||
|
"sourceLocale": "en-US",
|
||||||
|
"locales": {
|
||||||
|
"ar-AR": "src/locale/messages.ar_AR.xlf",
|
||||||
|
"be-BY": "src/locale/messages.be_BY.xlf",
|
||||||
|
"cs-CZ": "src/locale/messages.cs_CZ.xlf",
|
||||||
|
"da-DK": "src/locale/messages.da_DK.xlf",
|
||||||
|
"de-DE": "src/locale/messages.de_DE.xlf",
|
||||||
|
"en-GB": "src/locale/messages.en_GB.xlf",
|
||||||
|
"es-ES": "src/locale/messages.es_ES.xlf",
|
||||||
|
"fr-FR": "src/locale/messages.fr_FR.xlf",
|
||||||
|
"it-IT": "src/locale/messages.it_IT.xlf",
|
||||||
|
"lb-LU": "src/locale/messages.lb_LU.xlf",
|
||||||
|
"nl-NL": "src/locale/messages.nl_NL.xlf",
|
||||||
|
"pl-PL": "src/locale/messages.pl_PL.xlf",
|
||||||
|
"pt-BR": "src/locale/messages.pt_BR.xlf",
|
||||||
|
"pt-PT": "src/locale/messages.pt_PT.xlf",
|
||||||
|
"ro-RO": "src/locale/messages.ro_RO.xlf",
|
||||||
|
"ru-RU": "src/locale/messages.ru_RU.xlf",
|
||||||
|
"sl-SI": "src/locale/messages.sl_SI.xlf",
|
||||||
|
"sr-CS": "src/locale/messages.sr_CS.xlf",
|
||||||
|
"sv-SE": "src/locale/messages.sv_SE.xlf",
|
||||||
|
"tr-TR": "src/locale/messages.tr_TR.xlf",
|
||||||
|
"zh-CN": "src/locale/messages.zh_CN.xlf"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/paperless-ui",
|
||||||
|
"outputHashing": "none",
|
||||||
|
"index": "src/index.html",
|
||||||
|
"main": "src/main.ts",
|
||||||
|
"polyfills": "src/polyfills.ts",
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"localize": true,
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/apple-touch-icon.png",
|
||||||
|
"src/assets",
|
||||||
|
"src/manifest.webmanifest",
|
||||||
|
{
|
||||||
|
"glob": "pdf.worker.min.js",
|
||||||
|
"input": "node_modules/pdfjs-dist/build/",
|
||||||
|
"output": "/assets/js/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.scss"
|
||||||
|
],
|
||||||
|
"scripts": [],
|
||||||
|
"allowedCommonJsDependencies": [
|
||||||
|
"ng2-pdf-viewer"
|
||||||
|
],
|
||||||
|
"vendorChunk": true,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"buildOptimizer": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"optimization": false,
|
||||||
|
"namedChunks": true
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputPath": "../src/documents/static/frontend/",
|
||||||
|
"optimization": true,
|
||||||
|
"outputHashing": "none",
|
||||||
|
"sourceMap": false,
|
||||||
|
"namedChunks": false,
|
||||||
|
"extractLicenses": true,
|
||||||
|
"vendorChunk": false,
|
||||||
|
"buildOptimizer": true,
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "2mb",
|
||||||
|
"maximumError": "5mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "6kb",
|
||||||
|
"maximumError": "10kb"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"en-US": {
|
||||||
|
"localize": [
|
||||||
|
"en-US"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": ""
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "paperless-ui:build:en-US"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"browserTarget": "paperless-ui:build:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "paperless-ui:build"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular-builders/jest:run",
|
||||||
|
"options": {
|
||||||
|
"tsConfig": "tsconfig.spec.json",
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/apple-touch-icon.png",
|
||||||
|
"src/assets",
|
||||||
|
"src/manifest.webmanifest"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.scss"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"e2e": {
|
||||||
|
"builder": "@cypress/schematic:cypress",
|
||||||
|
"options": {
|
||||||
|
"devServerTarget": "paperless-ui:serve",
|
||||||
|
"watch": true,
|
||||||
|
"headless": false
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"devServerTarget": "paperless-ui:serve:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cypress-run": {
|
||||||
|
"builder": "@cypress/schematic:cypress",
|
||||||
|
"options": {
|
||||||
|
"devServerTarget": "paperless-ui:serve"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"devServerTarget": "paperless-ui:serve:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cypress-open": {
|
||||||
|
"builder": "@cypress/schematic:cypress",
|
||||||
|
"options": {
|
||||||
|
"watch": true,
|
||||||
|
"headless": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"builder": "@angular-eslint/builder:lint",
|
||||||
|
"options": {
|
||||||
|
"lintFilePatterns": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.html"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultProject": "paperless-ui",
|
||||||
|
"cli": {
|
||||||
|
"schematicCollections": [
|
||||||
|
"@angular-eslint/schematics"
|
||||||
|
],
|
||||||
|
"analytics": false
|
||||||
|
},
|
||||||
|
"schematics": {
|
||||||
|
"@angular-eslint/schematics:application": {
|
||||||
|
"setParserOptionsProject": true
|
||||||
|
},
|
||||||
|
"@angular-eslint/schematics:library": {
|
||||||
|
"setParserOptionsProject": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
src-ui/cypress.config.ts
Normal file
14
src-ui/cypress.config.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'cypress'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
videosFolder: 'cypress/videos',
|
||||||
|
video: false,
|
||||||
|
screenshotsFolder: 'cypress/screenshots',
|
||||||
|
fixturesFolder: 'cypress/fixtures',
|
||||||
|
e2e: {
|
||||||
|
setupNodeEvents(on, config) {
|
||||||
|
return require('./cypress/plugins/index.ts')(on, config)
|
||||||
|
},
|
||||||
|
baseUrl: 'http://localhost:4200',
|
||||||
|
},
|
||||||
|
})
|
68
src-ui/cypress/e2e/auth/auth.cy.ts
Normal file
68
src-ui/cypress/e2e/auth/auth.cy.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
describe('settings', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// also uses global fixtures from cypress/support/e2e.ts
|
||||||
|
|
||||||
|
// mock restricted permissions
|
||||||
|
cy.intercept('http://localhost:8000/api/ui_settings/', {
|
||||||
|
fixture: 'ui_settings/settings_restricted.json',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not allow user to edit settings', () => {
|
||||||
|
cy.visit('/dashboard')
|
||||||
|
cy.contains('Settings').should('not.exist')
|
||||||
|
cy.visit('/settings').wait(2000)
|
||||||
|
cy.contains("You don't have permissions to do that").should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not allow user to view documents', () => {
|
||||||
|
cy.visit('/dashboard')
|
||||||
|
cy.contains('Documents').should('not.exist')
|
||||||
|
cy.visit('/documents').wait(2000)
|
||||||
|
cy.contains("You don't have permissions to do that").should('exist')
|
||||||
|
cy.visit('/documents/1').wait(2000)
|
||||||
|
cy.contains("You don't have permissions to do that").should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not allow user to view correspondents', () => {
|
||||||
|
cy.visit('/dashboard')
|
||||||
|
cy.contains('Correspondents').should('not.exist')
|
||||||
|
cy.visit('/correspondents').wait(2000)
|
||||||
|
cy.contains("You don't have permissions to do that").should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not allow user to view tags', () => {
|
||||||
|
cy.visit('/dashboard')
|
||||||
|
cy.contains('Tags').should('not.exist')
|
||||||
|
cy.visit('/tags').wait(2000)
|
||||||
|
cy.contains("You don't have permissions to do that").should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not allow user to view document types', () => {
|
||||||
|
cy.visit('/dashboard')
|
||||||
|
cy.contains('Document Types').should('not.exist')
|
||||||
|
cy.visit('/documenttypes').wait(2000)
|
||||||
|
cy.contains("You don't have permissions to do that").should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not allow user to view storage paths', () => {
|
||||||
|
cy.visit('/dashboard')
|
||||||
|
cy.contains('Storage Paths').should('not.exist')
|
||||||
|
cy.visit('/storagepaths').wait(2000)
|
||||||
|
cy.contains("You don't have permissions to do that").should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not allow user to view logs', () => {
|
||||||
|
cy.visit('/dashboard')
|
||||||
|
cy.contains('Logs').should('not.exist')
|
||||||
|
cy.visit('/logs').wait(2000)
|
||||||
|
cy.contains("You don't have permissions to do that").should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not allow user to view tasks', () => {
|
||||||
|
cy.visit('/dashboard')
|
||||||
|
cy.contains('Tasks').should('not.exist')
|
||||||
|
cy.visit('/tasks').wait(2000)
|
||||||
|
cy.contains("You don't have permissions to do that").should('exist')
|
||||||
|
})
|
||||||
|
})
|
114
src-ui/cypress/e2e/documents/document-detail.cy.ts
Normal file
114
src-ui/cypress/e2e/documents/document-detail.cy.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
describe('document-detail', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// also uses global fixtures from cypress/support/e2e.ts
|
||||||
|
|
||||||
|
this.modifiedDocuments = []
|
||||||
|
|
||||||
|
cy.fixture('documents/documents.json').then((documentsJson) => {
|
||||||
|
cy.intercept('GET', 'http://localhost:8000/api/documents/1/', (req) => {
|
||||||
|
let response = { ...documentsJson }
|
||||||
|
response = response.results.find((d) => d.id == 1)
|
||||||
|
req.reply(response)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('PUT', 'http://localhost:8000/api/documents/1/', (req) => {
|
||||||
|
this.modifiedDocuments.push(req.body) // store this for later
|
||||||
|
req.reply({ result: 'OK' })
|
||||||
|
}).as('saveDoc')
|
||||||
|
|
||||||
|
cy.fixture('documents/1/notes.json').then((notesJson) => {
|
||||||
|
cy.intercept(
|
||||||
|
'GET',
|
||||||
|
'http://localhost:8000/api/documents/1/notes/',
|
||||||
|
(req) => {
|
||||||
|
req.reply(notesJson.filter((c) => c.id != 10)) // 3
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
cy.intercept(
|
||||||
|
'DELETE',
|
||||||
|
'http://localhost:8000/api/documents/1/notes/?id=9',
|
||||||
|
(req) => {
|
||||||
|
req.reply(notesJson.filter((c) => c.id != 9 && c.id != 10)) // 2
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
cy.intercept(
|
||||||
|
'POST',
|
||||||
|
'http://localhost:8000/api/documents/1/notes/',
|
||||||
|
(req) => {
|
||||||
|
req.reply(notesJson) // 4
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.viewport(1024, 1024)
|
||||||
|
cy.visit('/documents/1/').wait('@ui-settings')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should activate / deactivate save button when changes are saved', () => {
|
||||||
|
cy.contains('button', 'Save').should('be.disabled')
|
||||||
|
cy.get('app-input-text[formcontrolname="title"]')
|
||||||
|
.type(' additional')
|
||||||
|
.wait(1500) // this delay is for frontend debounce
|
||||||
|
cy.contains('button', 'Save').should('not.be.disabled')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should warn on unsaved changes', () => {
|
||||||
|
cy.get('app-input-text[formcontrolname="title"]')
|
||||||
|
.type(' additional')
|
||||||
|
.wait(1500) // this delay is for frontend debounce
|
||||||
|
cy.get('button[title="Close"]').click()
|
||||||
|
cy.contains('You have unsaved changes')
|
||||||
|
cy.contains('button', 'Cancel').click().wait(150)
|
||||||
|
cy.contains('button', 'Save').click().wait('@saveDoc').wait(2000) // navigates away after saving
|
||||||
|
cy.contains('You have unsaved changes').should('not.exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a mobile preview', () => {
|
||||||
|
cy.viewport(440, 1000)
|
||||||
|
cy.get('a')
|
||||||
|
.contains('Preview')
|
||||||
|
.scrollIntoView({ offset: { top: 150, left: 0 } })
|
||||||
|
.click()
|
||||||
|
cy.get('pdf-viewer').should('be.visible')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of notes', () => {
|
||||||
|
cy.wait(1000).get('a').contains('Notes').click({ force: true }).wait(1000)
|
||||||
|
cy.get('app-document-notes').find('.card').its('length').should('eq', 3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support note deletion', () => {
|
||||||
|
cy.wait(1000).get('a').contains('Notes').click().wait(1000)
|
||||||
|
cy.get('app-document-notes')
|
||||||
|
.find('.card')
|
||||||
|
.first()
|
||||||
|
.find('button')
|
||||||
|
.click({ force: true })
|
||||||
|
.wait(500)
|
||||||
|
cy.get('app-document-notes').find('.card').its('length').should('eq', 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support note insertion', () => {
|
||||||
|
cy.wait(1000).get('a').contains('Notes').click().wait(1000)
|
||||||
|
cy.get('app-document-notes')
|
||||||
|
.find('form textarea')
|
||||||
|
.type('Testing new note')
|
||||||
|
.wait(500)
|
||||||
|
cy.get('app-document-notes').find('form button').click().wait(1500)
|
||||||
|
cy.get('app-document-notes').find('.card').its('length').should('eq', 4)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support navigation to notes tab by url', () => {
|
||||||
|
cy.visit('/documents/1/notes')
|
||||||
|
cy.get('app-document-notes').should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should dynamically update note counts', () => {
|
||||||
|
cy.visit('/documents/1/notes')
|
||||||
|
cy.get('app-document-notes').within(() => cy.contains('Delete').click())
|
||||||
|
cy.get('ul.nav').find('li').contains('Notes').find('.badge').contains('2')
|
||||||
|
})
|
||||||
|
})
|
196
src-ui/cypress/e2e/documents/documents-list.cy.ts
Normal file
196
src-ui/cypress/e2e/documents/documents-list.cy.ts
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
describe('documents-list', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// also uses global fixtures from cypress/support/e2e.ts
|
||||||
|
|
||||||
|
this.bulkEdits = {}
|
||||||
|
|
||||||
|
cy.fixture('documents/documents.json').then((documentsJson) => {
|
||||||
|
// bulk edit
|
||||||
|
cy.intercept(
|
||||||
|
'POST',
|
||||||
|
'http://localhost:8000/api/documents/bulk_edit/',
|
||||||
|
(req) => {
|
||||||
|
this.bulkEdits = req.body // store this for later
|
||||||
|
req.reply({ result: 'OK' })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
cy.intercept('GET', 'http://localhost:8000/api/documents/*', (req) => {
|
||||||
|
let response = { ...documentsJson }
|
||||||
|
|
||||||
|
// bulkEdits was set earlier by bulk_edit intercept
|
||||||
|
if (this.bulkEdits.hasOwnProperty('documents')) {
|
||||||
|
response.results = response.results.map((d) => {
|
||||||
|
if ((this.bulkEdits['documents'] as Array<number>).includes(d.id)) {
|
||||||
|
switch (this.bulkEdits['method']) {
|
||||||
|
case 'modify_tags':
|
||||||
|
d.tags = (d.tags as Array<number>).concat([
|
||||||
|
this.bulkEdits['parameters']['add_tags'],
|
||||||
|
])
|
||||||
|
break
|
||||||
|
case 'set_correspondent':
|
||||||
|
d.correspondent =
|
||||||
|
this.bulkEdits['parameters']['correspondent']
|
||||||
|
break
|
||||||
|
case 'set_document_type':
|
||||||
|
d.document_type =
|
||||||
|
this.bulkEdits['parameters']['document_type']
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return d
|
||||||
|
})
|
||||||
|
} else if (req.query.hasOwnProperty('tags__id__all')) {
|
||||||
|
// filtering e.g. http://localhost:8000/api/documents/?page=1&page_size=50&ordering=-created&tags__id__all=2
|
||||||
|
const tag_id = +req.query['tags__id__all']
|
||||||
|
response.results = (documentsJson.results as Array<any>).filter((d) =>
|
||||||
|
(d.tags as Array<number>).includes(tag_id)
|
||||||
|
)
|
||||||
|
response.count = response.results.length
|
||||||
|
} else if (req.query.hasOwnProperty('correspondent__id__in')) {
|
||||||
|
// filtering e.g. http://localhost:8000/api/documents/?page=1&page_size=50&ordering=-created&correspondent__id__in=9,14
|
||||||
|
const correspondent_ids = req.query['correspondent__id__in']
|
||||||
|
.toString()
|
||||||
|
.split(',')
|
||||||
|
.map((c) => +c)
|
||||||
|
response.results = (documentsJson.results as Array<any>).filter((d) =>
|
||||||
|
correspondent_ids.includes(d.correspondent)
|
||||||
|
)
|
||||||
|
response.count = response.results.length
|
||||||
|
} else if (req.query.hasOwnProperty('correspondent__id__none')) {
|
||||||
|
// filtering e.g. http://localhost:8000/api/documents/?page=1&page_size=50&ordering=-created&correspondent__id__none=9,14
|
||||||
|
const correspondent_ids = req.query['correspondent__id__none']
|
||||||
|
.toString()
|
||||||
|
.split(',')
|
||||||
|
.map((c) => +c)
|
||||||
|
response.results = (documentsJson.results as Array<any>).filter(
|
||||||
|
(d) => !correspondent_ids.includes(d.correspondent)
|
||||||
|
)
|
||||||
|
response.count = response.results.length
|
||||||
|
}
|
||||||
|
|
||||||
|
req.reply(response)
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/documents/selection_data/', {
|
||||||
|
fixture: 'documents/selection_data.json',
|
||||||
|
}).as('selection-data')
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.viewport(1280, 1024)
|
||||||
|
cy.visit('/documents')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents rendered as cards with thumbnails', () => {
|
||||||
|
cy.contains('3 documents')
|
||||||
|
cy.contains('lorem ipsum')
|
||||||
|
cy.get('app-document-card-small:first-of-type img')
|
||||||
|
.invoke('attr', 'src')
|
||||||
|
.should('eq', 'http://localhost:8000/api/documents/1/thumb/')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should change to table "details" view', () => {
|
||||||
|
cy.get('div.btn-group input[value="details"]').next().click()
|
||||||
|
cy.get('table')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should change to large cards view', () => {
|
||||||
|
cy.get('div.btn-group input[value="largeCards"]').next().click()
|
||||||
|
cy.get('app-document-card-large')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show partial tag selection', () => {
|
||||||
|
cy.get('app-document-card-small:nth-child(1)').click()
|
||||||
|
cy.get('app-document-card-small:nth-child(4)').click()
|
||||||
|
cy.get('app-bulk-editor button')
|
||||||
|
.contains('Tags')
|
||||||
|
.click()
|
||||||
|
.wait('@selection-data')
|
||||||
|
cy.get('svg.bi-dash').should('be.visible')
|
||||||
|
cy.get('svg.bi-check').should('be.visible')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should allow bulk removal', () => {
|
||||||
|
cy.get('app-document-card-small:nth-child(1)').click()
|
||||||
|
cy.get('app-document-card-small:nth-child(4)').click()
|
||||||
|
cy.get('app-bulk-editor').within(() => {
|
||||||
|
cy.get('button').contains('Tags').click().wait('@selection-data')
|
||||||
|
cy.get('button').contains('Another Sample Tag').click()
|
||||||
|
cy.get('button').contains('Apply').click()
|
||||||
|
})
|
||||||
|
cy.contains('operation will remove the tag')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should filter tags', () => {
|
||||||
|
cy.get('app-filter-editor app-filterable-dropdown[title="Tags"]').within(
|
||||||
|
() => {
|
||||||
|
cy.contains('button', 'Tags').click()
|
||||||
|
cy.contains('button', 'Tag 2').click()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
cy.contains('One document')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should filter including multiple correspondents', () => {
|
||||||
|
cy.get('app-filter-editor app-filterable-dropdown[title="Correspondent"]')
|
||||||
|
.click()
|
||||||
|
.within(() => {
|
||||||
|
cy.contains('button', 'ABC Test Correspondent').click()
|
||||||
|
cy.contains('button', 'Corresp 11').click()
|
||||||
|
})
|
||||||
|
cy.contains('3 documents')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should filter excluding multiple correspondents', () => {
|
||||||
|
cy.get('app-filter-editor app-filterable-dropdown[title="Correspondent"]')
|
||||||
|
.click()
|
||||||
|
.within(() => {
|
||||||
|
cy.contains('button', 'ABC Test Correspondent').click()
|
||||||
|
cy.contains('button', 'Corresp 11').click()
|
||||||
|
cy.contains('label', 'Exclude').click()
|
||||||
|
})
|
||||||
|
cy.contains('One document')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should apply tags', () => {
|
||||||
|
cy.get('app-document-card-small:first-of-type').click()
|
||||||
|
cy.get('app-bulk-editor app-filterable-dropdown[title="Tags"]').within(
|
||||||
|
() => {
|
||||||
|
cy.contains('button', 'Tags').click()
|
||||||
|
cy.contains('button', 'Test Tag').click()
|
||||||
|
cy.contains('button', 'Apply').click()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
cy.contains('button', 'Confirm').click()
|
||||||
|
cy.get('app-document-card-small:first-of-type').contains('Test Tag')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should apply correspondent', () => {
|
||||||
|
cy.get('app-document-card-small:first-of-type').click()
|
||||||
|
cy.get(
|
||||||
|
'app-bulk-editor app-filterable-dropdown[title="Correspondent"]'
|
||||||
|
).within(() => {
|
||||||
|
cy.contains('button', 'Correspondent').click()
|
||||||
|
cy.contains('button', 'ABC Test Correspondent').click()
|
||||||
|
cy.contains('button', 'Apply').click()
|
||||||
|
})
|
||||||
|
cy.contains('button', 'Confirm').click()
|
||||||
|
cy.get('app-document-card-small:first-of-type').contains(
|
||||||
|
'ABC Test Correspondent'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should apply document type', () => {
|
||||||
|
cy.get('app-document-card-small:first-of-type').click()
|
||||||
|
cy.get(
|
||||||
|
'app-bulk-editor app-filterable-dropdown[title="Document type"]'
|
||||||
|
).within(() => {
|
||||||
|
cy.contains('button', 'Document type').click()
|
||||||
|
cy.contains('button', 'Test Doc Type').click()
|
||||||
|
cy.contains('button', 'Apply').click()
|
||||||
|
})
|
||||||
|
cy.contains('button', 'Confirm').click()
|
||||||
|
cy.get('app-document-card-small:first-of-type').contains('Test Doc Type')
|
||||||
|
})
|
||||||
|
})
|
341
src-ui/cypress/e2e/documents/query-params.cy.ts
Normal file
341
src-ui/cypress/e2e/documents/query-params.cy.ts
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
import { PaperlessDocument } from 'src/app/data/paperless-document'
|
||||||
|
|
||||||
|
describe('documents query params', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// also uses global fixtures from cypress/support/e2e.ts
|
||||||
|
|
||||||
|
cy.fixture('documents/documents.json').then((documentsJson) => {
|
||||||
|
// mock api filtering
|
||||||
|
cy.intercept('GET', 'http://localhost:8000/api/documents/*', (req) => {
|
||||||
|
let response = { ...documentsJson }
|
||||||
|
|
||||||
|
if (req.query.hasOwnProperty('ordering')) {
|
||||||
|
const sort_field = req.query['ordering'].toString().replace('-', '')
|
||||||
|
const reverse = req.query['ordering'].toString().indexOf('-') !== -1
|
||||||
|
response.results = (
|
||||||
|
documentsJson.results as Array<PaperlessDocument>
|
||||||
|
).sort((docA, docB) => {
|
||||||
|
let result = 0
|
||||||
|
switch (sort_field) {
|
||||||
|
case 'created':
|
||||||
|
case 'added':
|
||||||
|
result =
|
||||||
|
new Date(docA[sort_field]) < new Date(docB[sort_field])
|
||||||
|
? -1
|
||||||
|
: 1
|
||||||
|
break
|
||||||
|
case 'archive_serial_number':
|
||||||
|
result = docA[sort_field] < docB[sort_field] ? -1 : 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (reverse) result = -result
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.query.hasOwnProperty('tags__id__in')) {
|
||||||
|
const tag_ids: Array<number> = req.query['tags__id__in']
|
||||||
|
.toString()
|
||||||
|
.split(',')
|
||||||
|
.map((v) => +v)
|
||||||
|
response.results = (
|
||||||
|
documentsJson.results as Array<PaperlessDocument>
|
||||||
|
).filter(
|
||||||
|
(d) =>
|
||||||
|
d.tags.length > 0 &&
|
||||||
|
d.tags.filter((t) => tag_ids.includes(t)).length > 0
|
||||||
|
)
|
||||||
|
response.count = response.results.length
|
||||||
|
} else if (req.query.hasOwnProperty('tags__id__none')) {
|
||||||
|
const tag_ids: Array<number> = req.query['tags__id__none']
|
||||||
|
.toString()
|
||||||
|
.split(',')
|
||||||
|
.map((v) => +v)
|
||||||
|
response.results = (
|
||||||
|
documentsJson.results as Array<PaperlessDocument>
|
||||||
|
).filter((d) => d.tags.filter((t) => tag_ids.includes(t)).length == 0)
|
||||||
|
response.count = response.results.length
|
||||||
|
} else if (
|
||||||
|
req.query.hasOwnProperty('is_tagged') &&
|
||||||
|
req.query['is_tagged'] == '0'
|
||||||
|
) {
|
||||||
|
response.results = (
|
||||||
|
documentsJson.results as Array<PaperlessDocument>
|
||||||
|
).filter((d) => d.tags.length == 0)
|
||||||
|
response.count = response.results.length
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.query.hasOwnProperty('document_type__id')) {
|
||||||
|
const doctype_id = +req.query['document_type__id']
|
||||||
|
response.results = (
|
||||||
|
documentsJson.results as Array<PaperlessDocument>
|
||||||
|
).filter((d) => d.document_type == doctype_id)
|
||||||
|
response.count = response.results.length
|
||||||
|
} else if (
|
||||||
|
req.query.hasOwnProperty('document_type__isnull') &&
|
||||||
|
req.query['document_type__isnull'] == '1'
|
||||||
|
) {
|
||||||
|
response.results = (
|
||||||
|
documentsJson.results as Array<PaperlessDocument>
|
||||||
|
).filter((d) => d.document_type == undefined)
|
||||||
|
response.count = response.results.length
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.query.hasOwnProperty('correspondent__id')) {
|
||||||
|
const correspondent_id = +req.query['correspondent__id']
|
||||||
|
response.results = (
|
||||||
|
documentsJson.results as Array<PaperlessDocument>
|
||||||
|
).filter((d) => d.correspondent == correspondent_id)
|
||||||
|
response.count = response.results.length
|
||||||
|
} else if (
|
||||||
|
req.query.hasOwnProperty('correspondent__isnull') &&
|
||||||
|
req.query['correspondent__isnull'] == '1'
|
||||||
|
) {
|
||||||
|
response.results = (
|
||||||
|
documentsJson.results as Array<PaperlessDocument>
|
||||||
|
).filter((d) => d.correspondent == undefined)
|
||||||
|
response.count = response.results.length
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.query.hasOwnProperty('storage_path__id')) {
|
||||||
|
const storage_path_id = +req.query['storage_path__id']
|
||||||
|
response.results = (
|
||||||
|
documentsJson.results as Array<PaperlessDocument>
|
||||||
|
).filter((d) => d.storage_path == storage_path_id)
|
||||||
|
response.count = response.results.length
|
||||||
|
} else if (
|
||||||
|
req.query.hasOwnProperty('storage_path__isnull') &&
|
||||||
|
req.query['storage_path__isnull'] == '1'
|
||||||
|
) {
|
||||||
|
response.results = (
|
||||||
|
documentsJson.results as Array<PaperlessDocument>
|
||||||
|
).filter((d) => d.storage_path == undefined)
|
||||||
|
response.count = response.results.length
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.query.hasOwnProperty('created__date__gt')) {
|
||||||
|
const date = new Date(req.query['created__date__gt'])
|
||||||
|
response.results = (
|
||||||
|
documentsJson.results as Array<PaperlessDocument>
|
||||||
|
).filter((d) => new Date(d.created) > date)
|
||||||
|
response.count = response.results.length
|
||||||
|
} else if (req.query.hasOwnProperty('created__date__lt')) {
|
||||||
|
const date = new Date(req.query['created__date__lt'])
|
||||||
|
response.results = (
|
||||||
|
documentsJson.results as Array<PaperlessDocument>
|
||||||
|
).filter((d) => new Date(d.created) < date)
|
||||||
|
response.count = response.results.length
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.query.hasOwnProperty('added__date__gt')) {
|
||||||
|
const date = new Date(req.query['added__date__gt'])
|
||||||
|
response.results = (
|
||||||
|
documentsJson.results as Array<PaperlessDocument>
|
||||||
|
).filter((d) => new Date(d.added) > date)
|
||||||
|
response.count = response.results.length
|
||||||
|
} else if (req.query.hasOwnProperty('added__date__lt')) {
|
||||||
|
const date = new Date(req.query['added__date__lt'])
|
||||||
|
response.results = (
|
||||||
|
documentsJson.results as Array<PaperlessDocument>
|
||||||
|
).filter((d) => new Date(d.added) < date)
|
||||||
|
response.count = response.results.length
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.query.hasOwnProperty('title_content')) {
|
||||||
|
const title_content_regexp = new RegExp(
|
||||||
|
req.query['title_content'].toString(),
|
||||||
|
'i'
|
||||||
|
)
|
||||||
|
response.results = (
|
||||||
|
documentsJson.results as Array<PaperlessDocument>
|
||||||
|
).filter(
|
||||||
|
(d) =>
|
||||||
|
title_content_regexp.test(d.title) ||
|
||||||
|
title_content_regexp.test(d.content)
|
||||||
|
)
|
||||||
|
response.count = response.results.length
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.query.hasOwnProperty('archive_serial_number')) {
|
||||||
|
const asn = +req.query['archive_serial_number']
|
||||||
|
response.results = (
|
||||||
|
documentsJson.results as Array<PaperlessDocument>
|
||||||
|
).filter((d) => d.archive_serial_number == asn)
|
||||||
|
response.count = response.results.length
|
||||||
|
} else if (req.query.hasOwnProperty('archive_serial_number__isnull')) {
|
||||||
|
const isnull = req.query['storage_path__isnull'] == '1'
|
||||||
|
response.results = (
|
||||||
|
documentsJson.results as Array<PaperlessDocument>
|
||||||
|
).filter((d) =>
|
||||||
|
isnull
|
||||||
|
? d.archive_serial_number == undefined
|
||||||
|
: d.archive_serial_number != undefined
|
||||||
|
)
|
||||||
|
response.count = response.results.length
|
||||||
|
} else if (req.query.hasOwnProperty('archive_serial_number__gt')) {
|
||||||
|
const asn = +req.query['archive_serial_number__gt']
|
||||||
|
response.results = (
|
||||||
|
documentsJson.results as Array<PaperlessDocument>
|
||||||
|
).filter(
|
||||||
|
(d) => d.archive_serial_number > 0 && d.archive_serial_number > asn
|
||||||
|
)
|
||||||
|
response.count = response.results.length
|
||||||
|
} else if (req.query.hasOwnProperty('archive_serial_number__lt')) {
|
||||||
|
const asn = +req.query['archive_serial_number__lt']
|
||||||
|
response.results = (
|
||||||
|
documentsJson.results as Array<PaperlessDocument>
|
||||||
|
).filter(
|
||||||
|
(d) => d.archive_serial_number > 0 && d.archive_serial_number < asn
|
||||||
|
)
|
||||||
|
response.count = response.results.length
|
||||||
|
}
|
||||||
|
|
||||||
|
req.reply(response)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents sorted by created', () => {
|
||||||
|
cy.visit('/documents?sort=created')
|
||||||
|
cy.get('app-document-card-small').first().contains('No latin title')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents reverse sorted by created', () => {
|
||||||
|
cy.visit('/documents?sort=created&reverse=true')
|
||||||
|
cy.get('app-document-card-small').first().contains('sit amet')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents sorted by added', () => {
|
||||||
|
cy.visit('/documents?sort=added')
|
||||||
|
cy.get('app-document-card-small').first().contains('No latin title')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents reverse sorted by added', () => {
|
||||||
|
cy.visit('/documents?sort=added&reverse=true')
|
||||||
|
cy.get('app-document-card-small').first().contains('sit amet')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents filtered by any tags', () => {
|
||||||
|
cy.visit('/documents?sort=created&reverse=true&tags__id__in=2,4,5')
|
||||||
|
cy.contains('3 documents')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents filtered by excluded tags', () => {
|
||||||
|
cy.visit('/documents?sort=created&reverse=true&tags__id__none=2,4')
|
||||||
|
cy.contains('One document')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents filtered by no tags', () => {
|
||||||
|
cy.visit('/documents?sort=created&reverse=true&is_tagged=0')
|
||||||
|
cy.contains('One document')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents filtered by document type', () => {
|
||||||
|
cy.visit('/documents?sort=created&reverse=true&document_type__id=1')
|
||||||
|
cy.contains('2 documents')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents filtered by multiple correspondents', () => {
|
||||||
|
cy.visit('/documents?sort=created&reverse=true&document_type__id__in=1,2')
|
||||||
|
cy.contains('3 documents')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents filtered by no document type', () => {
|
||||||
|
cy.visit('/documents?sort=created&reverse=true&document_type__isnull=1')
|
||||||
|
cy.contains('One document')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents filtered by correspondent', () => {
|
||||||
|
cy.visit('/documents?sort=created&reverse=true&correspondent__id=9')
|
||||||
|
cy.contains('2 documents')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents filtered by multiple correspondents', () => {
|
||||||
|
cy.visit('/documents?sort=created&reverse=true&correspondent__id__in=9,14')
|
||||||
|
cy.contains('3 documents')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents filtered by no correspondent', () => {
|
||||||
|
cy.visit('/documents?sort=created&reverse=true&correspondent__isnull=1')
|
||||||
|
cy.contains('One document')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents filtered by storage path', () => {
|
||||||
|
cy.visit('/documents?sort=created&reverse=true&storage_path__id=2')
|
||||||
|
cy.contains('One document')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents filtered by no storage path', () => {
|
||||||
|
cy.visit('/documents?sort=created&reverse=true&storage_path__isnull=1')
|
||||||
|
cy.contains('3 documents')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents filtered by title or content', () => {
|
||||||
|
cy.visit('/documents?sort=created&reverse=true&title_content=lorem')
|
||||||
|
cy.contains('2 documents')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents filtered by asn', () => {
|
||||||
|
cy.visit('/documents?sort=created&reverse=true&archive_serial_number=12345')
|
||||||
|
cy.contains('One document')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents filtered by empty asn', () => {
|
||||||
|
cy.visit(
|
||||||
|
'/documents?sort=created&reverse=true&archive_serial_number__isnull=1'
|
||||||
|
)
|
||||||
|
cy.contains('2 documents')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents filtered by non-empty asn', () => {
|
||||||
|
cy.visit(
|
||||||
|
'/documents?sort=created&reverse=true&archive_serial_number__isnull=0'
|
||||||
|
)
|
||||||
|
cy.contains('2 documents')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents filtered by asn greater than', () => {
|
||||||
|
cy.visit(
|
||||||
|
'/documents?sort=created&reverse=true&archive_serial_number__gt=12346'
|
||||||
|
)
|
||||||
|
cy.contains('One document')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents filtered by asn less than', () => {
|
||||||
|
cy.visit(
|
||||||
|
'/documents?sort=created&reverse=true&archive_serial_number__lt=12346'
|
||||||
|
)
|
||||||
|
cy.contains('One document')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents filtered by created date greater than', () => {
|
||||||
|
cy.visit(
|
||||||
|
'/documents?sort=created&reverse=true&created__date__gt=2022-03-23'
|
||||||
|
)
|
||||||
|
cy.contains('3 documents')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents filtered by created date less than', () => {
|
||||||
|
cy.visit(
|
||||||
|
'/documents?sort=created&reverse=true&created__date__lt=2022-03-23'
|
||||||
|
)
|
||||||
|
cy.contains('One document')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents filtered by added date greater than', () => {
|
||||||
|
cy.visit('/documents?sort=created&reverse=true&added__date__gt=2022-03-24')
|
||||||
|
cy.contains('2 documents')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents filtered by added date less than', () => {
|
||||||
|
cy.visit('/documents?sort=created&reverse=true&added__date__lt=2022-03-24')
|
||||||
|
cy.contains('2 documents')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents filtered by multiple filters', () => {
|
||||||
|
cy.visit(
|
||||||
|
'/documents?sort=created&reverse=true&document_type__id=1&correspondent__id=9&tags__id__in=4,5'
|
||||||
|
)
|
||||||
|
cy.contains('2 documents')
|
||||||
|
})
|
||||||
|
})
|
25
src-ui/cypress/e2e/manage/manage.cy.ts
Normal file
25
src-ui/cypress/e2e/manage/manage.cy.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
describe('manage', () => {
|
||||||
|
// also uses global fixtures from cypress/support/e2e.ts
|
||||||
|
|
||||||
|
it('should show a list of correspondents with bottom pagination as well', () => {
|
||||||
|
cy.visit('/correspondents')
|
||||||
|
cy.get('tbody').find('tr').its('length').should('eq', 25)
|
||||||
|
cy.get('ngb-pagination').its('length').should('eq', 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of tags without bottom pagination', () => {
|
||||||
|
cy.visit('/tags')
|
||||||
|
cy.get('tbody').find('tr').its('length').should('eq', 8)
|
||||||
|
cy.get('ngb-pagination').its('length').should('eq', 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of documents filtered by tag', () => {
|
||||||
|
cy.intercept('http://localhost:8000/api/documents/*', (req) => {
|
||||||
|
if (req.url.indexOf('tags__id__all=4'))
|
||||||
|
req.reply({ count: 3, next: null, previous: null, results: [] })
|
||||||
|
})
|
||||||
|
cy.visit('/tags')
|
||||||
|
cy.get('tbody').find('button:visible').contains('Documents').first().click() // id = 4
|
||||||
|
cy.contains('3 documents')
|
||||||
|
})
|
||||||
|
})
|
182
src-ui/cypress/e2e/settings/settings.cy.ts
Normal file
182
src-ui/cypress/e2e/settings/settings.cy.ts
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
describe('settings', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// also uses global fixtures from cypress/support/e2e.ts
|
||||||
|
|
||||||
|
this.modifiedViews = []
|
||||||
|
|
||||||
|
// mock API methods
|
||||||
|
cy.intercept('http://localhost:8000/api/ui_settings/', {
|
||||||
|
fixture: 'ui_settings/settings.json',
|
||||||
|
}).then(() => {
|
||||||
|
cy.fixture('saved_views/savedviews.json').then((savedViewsJson) => {
|
||||||
|
// saved views PATCH
|
||||||
|
cy.intercept(
|
||||||
|
'PATCH',
|
||||||
|
'http://localhost:8000/api/saved_views/*',
|
||||||
|
(req) => {
|
||||||
|
this.modifiedViews.push(req.body) // store this for later
|
||||||
|
req.reply({ result: 'OK' })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
cy.intercept(
|
||||||
|
'GET',
|
||||||
|
'http://localhost:8000/api/saved_views/*',
|
||||||
|
(req) => {
|
||||||
|
let response = { ...savedViewsJson }
|
||||||
|
if (this.modifiedViews.length) {
|
||||||
|
response.results = response.results.map((v) => {
|
||||||
|
if (this.modifiedViews.find((mv) => mv.id == v.id))
|
||||||
|
v = this.modifiedViews.find((mv) => mv.id == v.id)
|
||||||
|
return v
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
req.reply(response)
|
||||||
|
}
|
||||||
|
).as('savedViews')
|
||||||
|
})
|
||||||
|
|
||||||
|
this.newMailAccounts = []
|
||||||
|
|
||||||
|
cy.intercept(
|
||||||
|
'POST',
|
||||||
|
'http://localhost:8000/api/mail_accounts/',
|
||||||
|
(req) => {
|
||||||
|
const newRule = req.body
|
||||||
|
newRule.id = 3
|
||||||
|
this.newMailAccounts.push(newRule) // store this for later
|
||||||
|
req.reply({ result: 'OK' })
|
||||||
|
}
|
||||||
|
).as('saveAccount')
|
||||||
|
|
||||||
|
cy.fixture('mail_accounts/mail_accounts.json').then(
|
||||||
|
(mailAccountsJson) => {
|
||||||
|
cy.intercept(
|
||||||
|
'GET',
|
||||||
|
'http://localhost:8000/api/mail_accounts/*',
|
||||||
|
(req) => {
|
||||||
|
console.log(req, this.newMailAccounts)
|
||||||
|
|
||||||
|
let response = { ...mailAccountsJson }
|
||||||
|
if (this.newMailAccounts.length) {
|
||||||
|
response.results = response.results.concat(this.newMailAccounts)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.reply(response)
|
||||||
|
}
|
||||||
|
).as('getAccounts')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
this.newMailRules = []
|
||||||
|
|
||||||
|
cy.intercept('POST', 'http://localhost:8000/api/mail_rules/', (req) => {
|
||||||
|
const newRule = req.body
|
||||||
|
newRule.id = 2
|
||||||
|
this.newMailRules.push(newRule) // store this for later
|
||||||
|
req.reply({ result: 'OK' })
|
||||||
|
}).as('saveRule')
|
||||||
|
|
||||||
|
cy.fixture('mail_rules/mail_rules.json').then((mailRulesJson) => {
|
||||||
|
cy.intercept('GET', 'http://localhost:8000/api/mail_rules/*', (req) => {
|
||||||
|
let response = { ...mailRulesJson }
|
||||||
|
if (this.newMailRules.length) {
|
||||||
|
response.results = response.results.concat(this.newMailRules)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.reply(response)
|
||||||
|
}).as('getRules')
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.fixture('documents/documents.json').then((documentsJson) => {
|
||||||
|
cy.intercept('GET', 'http://localhost:8000/api/documents/1/', (req) => {
|
||||||
|
let response = { ...documentsJson }
|
||||||
|
response = response.results.find((d) => d.id == 1)
|
||||||
|
req.reply(response)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.viewport(1024, 1600)
|
||||||
|
cy.visit('/settings')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should activate / deactivate save button when settings change and are saved', () => {
|
||||||
|
cy.contains('button', 'Save').should('be.disabled')
|
||||||
|
cy.contains('Use system settings').click()
|
||||||
|
cy.contains('button', 'Save').should('not.be.disabled')
|
||||||
|
cy.contains('button', 'Save').click()
|
||||||
|
cy.contains('button', 'Save').should('be.disabled')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should warn on unsaved changes', () => {
|
||||||
|
cy.contains('Use system settings').click()
|
||||||
|
cy.contains('a', 'Dashboard').click()
|
||||||
|
cy.contains('You have unsaved changes')
|
||||||
|
cy.contains('button', 'Cancel').click()
|
||||||
|
cy.contains('button', 'Save').click().wait(2000)
|
||||||
|
cy.contains('a', 'Dashboard').click()
|
||||||
|
cy.contains('You have unsaved changes').should('not.exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should apply appearance changes when set', () => {
|
||||||
|
cy.contains('Use system settings').click()
|
||||||
|
cy.get('body').should('not.have.class', 'color-scheme-system')
|
||||||
|
cy.contains('Enable dark mode').click()
|
||||||
|
cy.get('body').should('have.class', 'color-scheme-dark')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove saved view from sidebar when unset', () => {
|
||||||
|
cy.contains('a', 'Saved views').click().wait(2000)
|
||||||
|
cy.get('#show_in_sidebar_1').click()
|
||||||
|
cy.contains('button', 'Save').click().wait('@savedViews').wait(2000)
|
||||||
|
cy.contains('li', 'Inbox').should('not.exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove saved view from dashboard when unset', () => {
|
||||||
|
cy.contains('a', 'Saved views').click()
|
||||||
|
cy.get('#show_on_dashboard_1').click()
|
||||||
|
cy.contains('button', 'Save').click().wait('@savedViews').wait(2000)
|
||||||
|
cy.visit('/dashboard')
|
||||||
|
cy.get('app-saved-view-widget').contains('Inbox').should('not.exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of mail accounts & rules & support creation', () => {
|
||||||
|
cy.contains('a', 'Mail').click()
|
||||||
|
cy.get('app-settings .tab-content ul li').its('length').should('eq', 5) // 2 headers, 2 accounts, 1 rule
|
||||||
|
cy.contains('button', 'Add Account').click()
|
||||||
|
cy.contains('Create new mail account')
|
||||||
|
cy.get('app-input-text[formcontrolname="name"]').type(
|
||||||
|
'Example Mail Account'
|
||||||
|
)
|
||||||
|
cy.get('app-input-text[formcontrolname="imap_server"]').type(
|
||||||
|
'mail.example.com'
|
||||||
|
)
|
||||||
|
cy.get('app-input-text[formcontrolname="imap_port"]').type('993')
|
||||||
|
cy.get('app-input-text[formcontrolname="username"]').type('username')
|
||||||
|
cy.get('app-input-password[formcontrolname="password"]').type('pass')
|
||||||
|
cy.contains('app-mail-account-edit-dialog button', 'Save')
|
||||||
|
.click()
|
||||||
|
.wait('@saveAccount')
|
||||||
|
.wait('@getAccounts')
|
||||||
|
cy.contains('Saved account')
|
||||||
|
|
||||||
|
cy.wait(1000)
|
||||||
|
cy.contains('button', 'Add Rule').click()
|
||||||
|
cy.contains('Create new mail rule')
|
||||||
|
cy.get('app-input-text[formcontrolname="name"]').type('Example Rule')
|
||||||
|
cy.get('app-input-select[formcontrolname="account"]').type('Example{enter}')
|
||||||
|
cy.get('app-input-number[formcontrolname="maximum_age"]').type('30')
|
||||||
|
cy.get('app-input-text[formcontrolname="filter_subject"]').type(
|
||||||
|
'[paperless]'
|
||||||
|
)
|
||||||
|
cy.contains('app-mail-rule-edit-dialog button', 'Save')
|
||||||
|
.click()
|
||||||
|
.wait('@saveRule')
|
||||||
|
.wait('@getRules')
|
||||||
|
cy.contains('Saved rule').wait(1000)
|
||||||
|
|
||||||
|
cy.get('app-settings .tab-content ul li').its('length').should('eq', 7)
|
||||||
|
})
|
||||||
|
})
|
93
src-ui/cypress/e2e/tasks/tasks.cy.ts
Normal file
93
src-ui/cypress/e2e/tasks/tasks.cy.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
describe('tasks', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
this.dismissedTasks = new Set<number>()
|
||||||
|
|
||||||
|
cy.fixture('tasks/tasks.json').then((tasksViewsJson) => {
|
||||||
|
// acknowledge tasks POST
|
||||||
|
cy.intercept(
|
||||||
|
'POST',
|
||||||
|
'http://localhost:8000/api/acknowledge_tasks/',
|
||||||
|
(req) => {
|
||||||
|
req.body['tasks'].forEach((t) => this.dismissedTasks.add(t)) // store this for later
|
||||||
|
req.reply({ result: 'OK' })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
cy.intercept('GET', 'http://localhost:8000/api/tasks/', (req) => {
|
||||||
|
let response = [...tasksViewsJson]
|
||||||
|
if (this.dismissedTasks.size) {
|
||||||
|
response = response.filter((t) => {
|
||||||
|
return !this.dismissedTasks.has(t.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
req.reply(response)
|
||||||
|
}).as('tasks')
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.visit('/tasks')
|
||||||
|
cy.wait('@tasks')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show a list of dismissable tasks in tabs', () => {
|
||||||
|
cy.get('tbody').find('tr:visible').its('length').should('eq', 10) // double because collapsible result tr
|
||||||
|
cy.wait(500) // stabilizes the test, for some reason...
|
||||||
|
cy.get('tbody')
|
||||||
|
.find('button:visible')
|
||||||
|
.contains('Dismiss')
|
||||||
|
.first()
|
||||||
|
.click()
|
||||||
|
.wait('@tasks')
|
||||||
|
.wait(2000)
|
||||||
|
.then(() => {
|
||||||
|
cy.get('tbody').find('tr:visible').its('length').should('eq', 8) // double because collapsible result tr
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should correctly switch between task tabs', () => {
|
||||||
|
cy.get('tbody').find('tr:visible').its('length').should('eq', 10) // double because collapsible result tr
|
||||||
|
cy.wait(500) // stabilizes the test, for some reason...
|
||||||
|
cy.get('app-tasks')
|
||||||
|
.find('a:visible')
|
||||||
|
.contains('Queued')
|
||||||
|
.first()
|
||||||
|
.click()
|
||||||
|
.wait(2000)
|
||||||
|
.then(() => {
|
||||||
|
cy.get('tbody').find('tr:visible').should('not.exist')
|
||||||
|
})
|
||||||
|
cy.get('app-tasks')
|
||||||
|
.find('a:visible')
|
||||||
|
.contains('Started')
|
||||||
|
.first()
|
||||||
|
.click()
|
||||||
|
.wait(2000)
|
||||||
|
.then(() => {
|
||||||
|
cy.get('tbody').find('tr:visible').its('length').should('eq', 2) // double because collapsible result tr
|
||||||
|
})
|
||||||
|
cy.get('app-tasks')
|
||||||
|
.find('a:visible')
|
||||||
|
.contains('Complete')
|
||||||
|
.first()
|
||||||
|
.click()
|
||||||
|
.wait('@tasks')
|
||||||
|
.wait(2000)
|
||||||
|
.then(() => {
|
||||||
|
cy.get('tbody').find('tr:visible').its('length').should('eq', 12) // double because collapsible result tr
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should allow toggling all tasks in list and warn on dismiss', () => {
|
||||||
|
cy.get('thead').find('input[type="checkbox"]').first().click()
|
||||||
|
cy.get('body').find('button').contains('Dismiss selected').first().click()
|
||||||
|
cy.contains('Confirm')
|
||||||
|
cy.get('.modal')
|
||||||
|
.contains('button', 'Dismiss')
|
||||||
|
.click()
|
||||||
|
.wait('@tasks')
|
||||||
|
.wait(2000)
|
||||||
|
.then(() => {
|
||||||
|
cy.get('tbody').find('tr:visible').should('not.exist')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
257
src-ui/cypress/fixtures/correspondents/correspondents.json
Normal file
257
src-ui/cypress/fixtures/correspondents/correspondents.json
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
{
|
||||||
|
"count": 27,
|
||||||
|
"next": "http://localhost:8000/api/correspondents/?page=2",
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"slug": "abc-test-correspondent",
|
||||||
|
"name": "ABC Test Correspondent",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13,
|
||||||
|
"slug": "corresp-10",
|
||||||
|
"name": "Corresp 10",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 14,
|
||||||
|
"slug": "corresp-11",
|
||||||
|
"name": "Corresp 11",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 15,
|
||||||
|
"slug": "corresp-12",
|
||||||
|
"name": "Corresp 12",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 16,
|
||||||
|
"slug": "corresp-13",
|
||||||
|
"name": "Corresp 13",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 18,
|
||||||
|
"slug": "corresp-15",
|
||||||
|
"name": "Corresp 15",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 19,
|
||||||
|
"slug": "corresp-16",
|
||||||
|
"name": "Corresp 16",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 20,
|
||||||
|
"slug": "corresp-17",
|
||||||
|
"name": "Corresp 17",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 21,
|
||||||
|
"slug": "corresp-18",
|
||||||
|
"name": "Corresp 18",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 22,
|
||||||
|
"slug": "corresp-19",
|
||||||
|
"name": "Corresp 19",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23,
|
||||||
|
"slug": "corresp-20",
|
||||||
|
"name": "Corresp 20",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 24,
|
||||||
|
"slug": "corresp-21",
|
||||||
|
"name": "Corresp 21",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 25,
|
||||||
|
"slug": "corresp-22",
|
||||||
|
"name": "Corresp 22",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 26,
|
||||||
|
"slug": "corresp-23",
|
||||||
|
"name": "Corresp 23",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"slug": "corresp-3",
|
||||||
|
"name": "Corresp 3",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"slug": "corresp-4",
|
||||||
|
"name": "Corresp 4",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"slug": "corresp-5",
|
||||||
|
"name": "Corresp 5",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"slug": "corresp-6",
|
||||||
|
"name": "Corresp 6",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"slug": "corresp-7",
|
||||||
|
"name": "Corresp 7",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"slug": "corresp-8",
|
||||||
|
"name": "Corresp 8",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"slug": "corresp-9",
|
||||||
|
"name": "Corresp 9",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 17,
|
||||||
|
"slug": "correspondent-14",
|
||||||
|
"name": "Correspondent 14",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 0,
|
||||||
|
"last_correspondence": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"slug": "correspondent-2",
|
||||||
|
"name": "Correspondent 2",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 7,
|
||||||
|
"last_correspondence": "2021-01-20T23:37:58.204614Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 27,
|
||||||
|
"slug": "correspondent-slug",
|
||||||
|
"name": "Correspondent Slug",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 1,
|
||||||
|
"last_correspondence": "2022-03-16T03:48:50.089624Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"slug": "newest-correspondent",
|
||||||
|
"name": "Newest Correspondent",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 1,
|
||||||
|
"last_correspondence": "2021-02-07T08:00:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
25
src-ui/cypress/fixtures/document_types/doctypes.json
Normal file
25
src-ui/cypress/fixtures/document_types/doctypes.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"count": 2,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"slug": "test",
|
||||||
|
"name": "Test Doc Type",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"slug": "test2",
|
||||||
|
"name": "Test Doc Type 2",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
1
src-ui/cypress/fixtures/documents/1/metadata.json
Normal file
1
src-ui/cypress/fixtures/documents/1/metadata.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"original_checksum":"e959bc7d593245d92685213264e962ba","original_size":963754,"original_mime_type":"application/pdf","media_filename":"2022/lorem-ipsum.pdf","has_archive_version":true,"original_metadata":[],"archive_checksum":"5a1f46a9150bcade978c764b039ce4d0","archive_media_filename":"2022/lorem-ipsum.pdf","archive_size":351160,"archive_metadata":[{"namespace":"http://ns.adobe.com/pdf/1.3/","prefix":"pdf","key":"Producer","value":"pikepdf5.0.1"},{"namespace":"http://ns.adobe.com/xap/1.0/","prefix":"xmp","key":"ModifyDate","value":"2022-03-22T04:53:18+00:00"},{"namespace":"http://ns.adobe.com/xap/1.0/","prefix":"xmp","key":"CreateDate","value":"2022-03-22T18:05:43+00:00"},{"namespace":"http://ns.adobe.com/xap/1.0/","prefix":"xmp","key":"CreatorTool","value":"ocrmypdf13.4.0/TesseractOCR-PDF4.1.1"},{"namespace":"http://ns.adobe.com/xap/1.0/mm/","prefix":"xmpMM","key":"DocumentID","value":"uuid:df27edcf-e34a-11f7-0000-8fa6067a3c04"},{"namespace":"http://purl.org/dc/elements/1.1/","prefix":"dc","key":"format","value":"application/pdf"},{"namespace":"http://purl.org/dc/elements/1.1/","prefix":"dc","key":"title","value":"ScannedDocument"},{"namespace":"http://www.aiim.org/pdfa/ns/id/","prefix":"pdfaid","key":"part","value":"2"},{"namespace":"http://www.aiim.org/pdfa/ns/id/","prefix":"pdfaid","key":"conformance","value":"B"},{"namespace":"http://purl.org/dc/elements/1.1/","prefix":"dc","key":"creator","value":"None"},{"namespace":"http://ns.adobe.com/xap/1.0/","prefix":"xmp","key":"MetadataDate","value":"2022-03-22T21:53:18.882551-07:00"}]}
|
26
src-ui/cypress/fixtures/documents/1/notes.json
Normal file
26
src-ui/cypress/fixtures/documents/1/notes.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"note": "Testing new note",
|
||||||
|
"created": "2022-08-08T04:24:55.176008Z",
|
||||||
|
"user": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"note": "Testing one more time",
|
||||||
|
"created": "2022-02-18T04:24:55.176008Z",
|
||||||
|
"user": 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"note": "Another note",
|
||||||
|
"created": "2021-11-08T04:24:47.925042Z",
|
||||||
|
"user": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"note": "Cupcake ipsum dolor sit amet cheesecake candy cookie tiramisu. Donut chocolate chupa chups macaroon brownie halvah pie cheesecake gummies. Sweet chocolate bar candy donut gummi bears bear claw liquorice bonbon shortbread.\n\nDonut chocolate bar candy wafer wafer tiramisu. Gummies chocolate cake muffin toffee carrot cake macaroon. Toffee toffee jelly beans danish lollipop cake.",
|
||||||
|
"created": "2021-02-08T02:37:49.724132Z",
|
||||||
|
"user": 3
|
||||||
|
}
|
||||||
|
]
|
1
src-ui/cypress/fixtures/documents/1/suggestions.json
Normal file
1
src-ui/cypress/fixtures/documents/1/suggestions.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"correspondents":[],"tags":[3],"document_types":[1]}
|
144
src-ui/cypress/fixtures/documents/documents.json
Normal file
144
src-ui/cypress/fixtures/documents/documents.json
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
{
|
||||||
|
"count": 3,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"correspondent": 9,
|
||||||
|
"document_type": 1,
|
||||||
|
"storage_path": null,
|
||||||
|
"title": "No latin title",
|
||||||
|
"content": "Test document PDF \n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla est purus, ultrices in porttitor \nin, accumsan non quam. Nam consectetur porttitor rhoncus. Curabitur eu est et leo feugiat \nauctor vel quis lorem. Ut et ligula dolor, sit amet consequat lorem. Aliquam porta eros sed \nvelit imperdiet egestas. Maecenas tempus eros ut diam ullamcorper id dictum libero \ntempor. Donec quis augue quis magna condimentum lobortis. Quisque imperdiet ipsum vel \nmagna viverra rutrum. Cras viverra molestie urna, vitae vestibulum turpis varius id. \nVestibulum mollis, arcu iaculis bibendum varius, velit sapien blandit metus, ac posuere lorem \nnulla ac dolor. Maecenas urna elit, tincidunt in dapibus nec, vehicula eu dui. Duis lacinia \nfringilla massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur \nridiculus mus. Ut consequat ultricies est, non rhoncus mauris congue porta. Vivamus viverra \nsuscipit felis eget condimentum. Cum sociis natoque penatibus et magnis dis parturient \nmontes, nascetur ridiculus mus. Integer bibendum sagittis ligula, non faucibus nulla volutpat \nvitae. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. \nIn aliquet quam et velit bibendum accumsan. Cum sociis natoque penatibus et magnis dis \nparturient montes, nascetur ridiculus mus. Vestibulum vitae ipsum nec arcu semper \nadipiscing at ac lacus. Praesent id pellentesque orci. Morbi congue viverra nisl nec rhoncus. \nInteger mattis, ipsum a tincidunt commodo, lacus arcu elementum elit, at mollis eros ante ac \nrisus. In volutpat, ante at pretium ultricies, velit magna suscipit enim, aliquet blandit massa \norci nec lorem. Nulla facilisi. Duis eu vehicula arcu. Nulla facilisi. Maecenas pellentesque \nvolutpat felis, quis tristique ligula luctus vel. Sed nec mi eros. Integer augue enim, sollicitudin \nullamcorper mattis eget, aliquam in est. Morbi sollicitudin libero nec augue dignissim ut \nconsectetur dui volutpat. Nulla facilisi. Mauris egestas vestibulum neque cursus tincidunt. \nDonec sit amet pulvinar orci. \nQuisque volutpat pharetra tincidunt. Fusce sapien arcu, molestie eget varius egestas, \nfaucibus ac urna. Sed at nisi in velit egestas aliquam ut a felis. Aenean malesuada iaculis nisl, \nut tempor lacus egestas consequat. Nam nibh lectus, gravida sed egestas ut, feugiat quis \ndolor. Donec eu leo enim, non laoreet ante. Morbi dictum tempor vulputate. Phasellus \nultricies risus vel augue sagittis euismod. Vivamus tincidunt placerat nisi in aliquam. Cras \nquis mi ac nunc pretium aliquam. Aenean elementum erat ac metus commodo rhoncus. \nAliquam nulla augue, porta non sagittis quis, accumsan vitae sem. Phasellus id lectus tortor, \neget pulvinar augue. Etiam eget velit ac purus fringilla blandit. Donec odio odio, sagittis sed \niaculis sed, consectetur eget sem. Lorem ipsum dolor sit amet, consectetur adipiscing elit. \nMaecenas accumsan velit vel turpis rutrum in sodales diam placerat. \nQuisque luctus ullamcorper velit sit amet lobortis. Etiam ligula felis, vulputate quis rhoncus \nnec, fermentum eget odio. Vivamus vel ipsum ac augue sodales mollis euismod nec tellus. \nFusce et augue rutrum nunc semper vehicula vel semper nisl. Nam laoreet euismod quam at \nvarius. Sed aliquet auctor nibh. Curabitur malesuada fermentum lacus vel accumsan. Duis \nornare scelerisque nulla, ac pulvinar ligula tempus sit amet. In placerat nulla ac ante \nscelerisque posuere. Phasellus at ante felis. Sed hendrerit risus a metus posuere rutrum. \nPhasellus eu augue dui. Proin in vestibulum ipsum. Aenean accumsan mollis sapien, ut \neleifend sem blandit at. Vivamus luctus mi eget lorem lobortis pharetra. Phasellus at tortor \nquam, a volutpat purus. Etiam sollicitudin arcu vel elit bibendum et imperdiet risus tincidunt. \nEtiam elit velit, posuere ut pulvinar ac, condimentum eget justo. Fusce a erat velit. Vivamus \nimperdiet ultrices orci in hendrerit.",
|
||||||
|
"tags": [
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"created": "2022-03-22T07:24:18Z",
|
||||||
|
"created_date": "2022-03-22",
|
||||||
|
"modified": "2022-03-22T07:24:23.264859Z",
|
||||||
|
"added": "2022-03-22T07:24:22.922631Z",
|
||||||
|
"archive_serial_number": null,
|
||||||
|
"original_file_name": "2022-03-22 no latin title.pdf",
|
||||||
|
"archived_file_name": "2022-03-22 no latin title.pdf",
|
||||||
|
"owner": null,
|
||||||
|
"permissions": {
|
||||||
|
"view": {
|
||||||
|
"users": [],
|
||||||
|
"groups": []
|
||||||
|
},
|
||||||
|
"change": {
|
||||||
|
"users": [],
|
||||||
|
"groups": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"note": "Testing one more time",
|
||||||
|
"created": "2022-02-18T04:24:55.176008Z",
|
||||||
|
"user": 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"note": "Another note",
|
||||||
|
"created": "2021-11-08T04:24:47.925042Z",
|
||||||
|
"user": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"note": "Cupcake ipsum dolor sit amet cheesecake candy cookie tiramisu. Donut chocolate chupa chups macaroon brownie halvah pie cheesecake gummies. Sweet chocolate bar candy donut gummi bears bear claw liquorice bonbon shortbread.\n\nDonut chocolate bar candy wafer wafer tiramisu. Gummies chocolate cake muffin toffee carrot cake macaroon. Toffee toffee jelly beans danish lollipop cake.",
|
||||||
|
"created": "2021-02-08T02:37:49.724132Z",
|
||||||
|
"user": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"correspondent": null,
|
||||||
|
"document_type": null,
|
||||||
|
"storage_path": 2,
|
||||||
|
"title": "lorem ipsum dolor sit amet",
|
||||||
|
"content": "Test document PDF",
|
||||||
|
"tags": [],
|
||||||
|
"created": "2022-03-23T07:24:18Z",
|
||||||
|
"created_date": "2022-03-23",
|
||||||
|
"modified": "2022-03-23T07:24:23.264859Z",
|
||||||
|
"added": "2022-03-23T07:24:22.922631Z",
|
||||||
|
"archive_serial_number": 12345,
|
||||||
|
"original_file_name": "2022-03-23 lorem ipsum dolor sit amet.pdf",
|
||||||
|
"archived_file_name": "2022-03-23 llorem ipsum dolor sit amet.pdf",
|
||||||
|
"owner": null,
|
||||||
|
"permissions": {
|
||||||
|
"view": {
|
||||||
|
"users": [],
|
||||||
|
"groups": []
|
||||||
|
},
|
||||||
|
"change": {
|
||||||
|
"users": [],
|
||||||
|
"groups": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notes": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"correspondent": 14,
|
||||||
|
"document_type": 1,
|
||||||
|
"storage_path": null,
|
||||||
|
"title": "dolor",
|
||||||
|
"content": "Test document PDF",
|
||||||
|
"tags": [
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"created": "2022-03-24T07:24:18Z",
|
||||||
|
"created_date": "2022-03-24",
|
||||||
|
"modified": "2022-03-24T07:24:23.264859Z",
|
||||||
|
"added": "2022-03-24T07:24:22.922631Z",
|
||||||
|
"archive_serial_number": null,
|
||||||
|
"original_file_name": "2022-03-24 dolor.pdf",
|
||||||
|
"archived_file_name": "2022-03-24 dolor.pdf",
|
||||||
|
"owner": null,
|
||||||
|
"permissions": {
|
||||||
|
"view": {
|
||||||
|
"users": [],
|
||||||
|
"groups": []
|
||||||
|
},
|
||||||
|
"change": {
|
||||||
|
"users": [],
|
||||||
|
"groups": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notes": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"correspondent": 9,
|
||||||
|
"document_type": 2,
|
||||||
|
"storage_path": null,
|
||||||
|
"title": "sit amet",
|
||||||
|
"content": "Test document PDF",
|
||||||
|
"tags": [
|
||||||
|
4, 5
|
||||||
|
],
|
||||||
|
"created": "2022-06-01T07:24:18Z",
|
||||||
|
"created_date": "2022-06-01",
|
||||||
|
"modified": "2022-06-01T07:24:23.264859Z",
|
||||||
|
"added": "2022-06-01T07:24:22.922631Z",
|
||||||
|
"archive_serial_number": 12347,
|
||||||
|
"original_file_name": "2022-06-01 sit amet.pdf",
|
||||||
|
"archived_file_name": "2022-06-01 sit amet.pdf",
|
||||||
|
"owner": null,
|
||||||
|
"permissions": {
|
||||||
|
"view": {
|
||||||
|
"users": [],
|
||||||
|
"groups": []
|
||||||
|
},
|
||||||
|
"change": {
|
||||||
|
"users": [],
|
||||||
|
"groups": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notes": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
BIN
src-ui/cypress/fixtures/documents/lorem-ipsum.png
Normal file
BIN
src-ui/cypress/fixtures/documents/lorem-ipsum.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 156 KiB |
293
src-ui/cypress/fixtures/documents/selection_data.json
Normal file
293
src-ui/cypress/fixtures/documents/selection_data.json
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
{
|
||||||
|
"selected_correspondents": [
|
||||||
|
{
|
||||||
|
"id": 62,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 75,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 55,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 56,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 73,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 58,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 44,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 42,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 74,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 54,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 29,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 71,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 68,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 82,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 34,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 41,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 51,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 46,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 40,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 43,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 80,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 70,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 52,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 67,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 53,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 32,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 63,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 35,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 45,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 38,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 79,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 48,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 72,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 78,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 39,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 57,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 61,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 81,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 77,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 69,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 36,
|
||||||
|
"document_count": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 31,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 30,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 50,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 49,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 60,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 47,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 66,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 37,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 28,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 59,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 33,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 76,
|
||||||
|
"document_count": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"selected_tags": [
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"document_count": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"document_count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"document_count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"document_count": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"selected_document_types": [
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"document_count": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"document_count": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"selected_storage_paths": []
|
||||||
|
}
|
119
src-ui/cypress/fixtures/groups/groups.json
Normal file
119
src-ui/cypress/fixtures/groups/groups.json
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
{
|
||||||
|
"count": 2,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"name": "Another Group",
|
||||||
|
"permissions": [
|
||||||
|
"add_user",
|
||||||
|
"change_user",
|
||||||
|
"delete_user",
|
||||||
|
"view_user",
|
||||||
|
"add_note",
|
||||||
|
"change_note",
|
||||||
|
"delete_note",
|
||||||
|
"view_note"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "First Group",
|
||||||
|
"permissions": [
|
||||||
|
"add_group",
|
||||||
|
"change_group",
|
||||||
|
"delete_group",
|
||||||
|
"view_group",
|
||||||
|
"add_permission",
|
||||||
|
"change_permission",
|
||||||
|
"delete_permission",
|
||||||
|
"view_permission",
|
||||||
|
"add_token",
|
||||||
|
"change_token",
|
||||||
|
"delete_token",
|
||||||
|
"view_token",
|
||||||
|
"add_tokenproxy",
|
||||||
|
"change_tokenproxy",
|
||||||
|
"delete_tokenproxy",
|
||||||
|
"view_tokenproxy",
|
||||||
|
"add_contenttype",
|
||||||
|
"change_contenttype",
|
||||||
|
"delete_contenttype",
|
||||||
|
"view_contenttype",
|
||||||
|
"add_chordcounter",
|
||||||
|
"change_chordcounter",
|
||||||
|
"delete_chordcounter",
|
||||||
|
"view_chordcounter",
|
||||||
|
"add_groupresult",
|
||||||
|
"change_groupresult",
|
||||||
|
"delete_groupresult",
|
||||||
|
"view_groupresult",
|
||||||
|
"add_taskresult",
|
||||||
|
"change_taskresult",
|
||||||
|
"delete_taskresult",
|
||||||
|
"view_taskresult",
|
||||||
|
"add_failure",
|
||||||
|
"change_failure",
|
||||||
|
"delete_failure",
|
||||||
|
"view_failure",
|
||||||
|
"add_ormq",
|
||||||
|
"change_ormq",
|
||||||
|
"delete_ormq",
|
||||||
|
"view_ormq",
|
||||||
|
"add_schedule",
|
||||||
|
"change_schedule",
|
||||||
|
"delete_schedule",
|
||||||
|
"view_schedule",
|
||||||
|
"add_success",
|
||||||
|
"change_success",
|
||||||
|
"delete_success",
|
||||||
|
"view_success",
|
||||||
|
"add_task",
|
||||||
|
"change_task",
|
||||||
|
"delete_task",
|
||||||
|
"view_task",
|
||||||
|
"add_note",
|
||||||
|
"change_note",
|
||||||
|
"delete_note",
|
||||||
|
"view_note",
|
||||||
|
"add_correspondent",
|
||||||
|
"change_correspondent",
|
||||||
|
"delete_correspondent",
|
||||||
|
"view_correspondent",
|
||||||
|
"add_document",
|
||||||
|
"change_document",
|
||||||
|
"delete_document",
|
||||||
|
"view_document",
|
||||||
|
"add_documenttype",
|
||||||
|
"change_documenttype",
|
||||||
|
"delete_documenttype",
|
||||||
|
"view_documenttype",
|
||||||
|
"add_frontendsettings",
|
||||||
|
"change_frontendsettings",
|
||||||
|
"delete_frontendsettings",
|
||||||
|
"view_frontendsettings",
|
||||||
|
"add_log",
|
||||||
|
"change_log",
|
||||||
|
"delete_log",
|
||||||
|
"view_log",
|
||||||
|
"add_savedview",
|
||||||
|
"change_savedview",
|
||||||
|
"delete_savedview",
|
||||||
|
"view_savedview",
|
||||||
|
"add_savedviewfilterrule",
|
||||||
|
"change_savedviewfilterrule",
|
||||||
|
"delete_savedviewfilterrule",
|
||||||
|
"view_savedviewfilterrule",
|
||||||
|
"add_taskattributes",
|
||||||
|
"change_taskattributes",
|
||||||
|
"delete_taskattributes",
|
||||||
|
"view_taskattributes",
|
||||||
|
"add_session",
|
||||||
|
"change_session",
|
||||||
|
"delete_session",
|
||||||
|
"view_session"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
27
src-ui/cypress/fixtures/mail_accounts/mail_accounts.json
Normal file
27
src-ui/cypress/fixtures/mail_accounts/mail_accounts.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"count": 2,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "IMAP Server",
|
||||||
|
"imap_server": "imap.example.com",
|
||||||
|
"imap_port": 993,
|
||||||
|
"imap_security": 2,
|
||||||
|
"username": "inbox@example.com",
|
||||||
|
"password": "pass",
|
||||||
|
"character_set": "UTF-8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Gmail",
|
||||||
|
"imap_server": "imap.gmail.com",
|
||||||
|
"imap_port": 993,
|
||||||
|
"imap_security": 2,
|
||||||
|
"username": "user@gmail.com",
|
||||||
|
"password": "pass",
|
||||||
|
"character_set": "UTF-8"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
31
src-ui/cypress/fixtures/mail_rules/mail_rules.json
Normal file
31
src-ui/cypress/fixtures/mail_rules/mail_rules.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Gmail",
|
||||||
|
"account": 2,
|
||||||
|
"folder": "INBOX",
|
||||||
|
"filter_from": null,
|
||||||
|
"filter_to": null,
|
||||||
|
"filter_subject": "[paperless]",
|
||||||
|
"filter_body": null,
|
||||||
|
"filter_attachment_filename": null,
|
||||||
|
"maximum_age": 30,
|
||||||
|
"action": 3,
|
||||||
|
"action_parameter": null,
|
||||||
|
"assign_title_from": 1,
|
||||||
|
"assign_tags": [
|
||||||
|
9
|
||||||
|
],
|
||||||
|
"assign_correspondent_from": 1,
|
||||||
|
"assign_correspondent": 2,
|
||||||
|
"assign_document_type": null,
|
||||||
|
"order": 0,
|
||||||
|
"attachment_type": 2,
|
||||||
|
"consumption_scope": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
{"version":"v1.7.1","update_available":false,"feature_is_set":true}
|
44
src-ui/cypress/fixtures/saved_views/savedviews.json
Normal file
44
src-ui/cypress/fixtures/saved_views/savedviews.json
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"count": 3,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Inbox",
|
||||||
|
"show_on_dashboard": true,
|
||||||
|
"show_in_sidebar": true,
|
||||||
|
"sort_field": "created",
|
||||||
|
"sort_reverse": true,
|
||||||
|
"filter_rules": [
|
||||||
|
{
|
||||||
|
"rule_type": 6,
|
||||||
|
"value": "18"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Recently Added",
|
||||||
|
"show_on_dashboard": true,
|
||||||
|
"show_in_sidebar": false,
|
||||||
|
"sort_field": "created",
|
||||||
|
"sort_reverse": true,
|
||||||
|
"filter_rules": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"name": "Taxes",
|
||||||
|
"show_on_dashboard": false,
|
||||||
|
"show_in_sidebar": true,
|
||||||
|
"sort_field": "created",
|
||||||
|
"sort_reverse": true,
|
||||||
|
"filter_rules": [
|
||||||
|
{
|
||||||
|
"rule_type": 6,
|
||||||
|
"value": "39"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
17
src-ui/cypress/fixtures/storage_paths/storage_paths.json
Normal file
17
src-ui/cypress/fixtures/storage_paths/storage_paths.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"slug": "year-title",
|
||||||
|
"name": "Year - Title",
|
||||||
|
"path": "{created_year}/{title}",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 6,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"document_count": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
103
src-ui/cypress/fixtures/tags/tags.json
Normal file
103
src-ui/cypress/fixtures/tags/tags.json
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
{
|
||||||
|
"count": 8,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"slug": "another-sample-tag",
|
||||||
|
"name": "Another Sample Tag",
|
||||||
|
"color": "#a6cee3",
|
||||||
|
"text_color": "#000000",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 6,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"is_inbox_tag": false,
|
||||||
|
"document_count": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"slug": "newone",
|
||||||
|
"name": "NewOne",
|
||||||
|
"color": "#9e4ad1",
|
||||||
|
"text_color": "#ffffff",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"is_inbox_tag": false,
|
||||||
|
"document_count": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"slug": "partial-tag",
|
||||||
|
"name": "Partial Tag",
|
||||||
|
"color": "#72dba7",
|
||||||
|
"text_color": "#000000",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"is_inbox_tag": false,
|
||||||
|
"document_count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"slug": "tag-2",
|
||||||
|
"name": "Tag 2",
|
||||||
|
"color": "#612db7",
|
||||||
|
"text_color": "#ffffff",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"is_inbox_tag": false,
|
||||||
|
"document_count": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"slug": "tag-3",
|
||||||
|
"name": "Tag 3",
|
||||||
|
"color": "#b2df8a",
|
||||||
|
"text_color": "#000000",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"is_inbox_tag": false,
|
||||||
|
"document_count": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"slug": "tagwithpartial",
|
||||||
|
"name": "TagWithPartial",
|
||||||
|
"color": "#3b2db4",
|
||||||
|
"text_color": "#ffffff",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 6,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"is_inbox_tag": false,
|
||||||
|
"document_count": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"slug": "test-another",
|
||||||
|
"name": "Test Another",
|
||||||
|
"color": "#3ccea5",
|
||||||
|
"text_color": "#000000",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 4,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"is_inbox_tag": false,
|
||||||
|
"document_count": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"slug": "test-tag",
|
||||||
|
"name": "Test Tag",
|
||||||
|
"color": "#fb9a99",
|
||||||
|
"text_color": "#000000",
|
||||||
|
"match": "",
|
||||||
|
"matching_algorithm": 1,
|
||||||
|
"is_insensitive": true,
|
||||||
|
"is_inbox_tag": false,
|
||||||
|
"document_count": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
142
src-ui/cypress/fixtures/tasks/tasks.json
Normal file
142
src-ui/cypress/fixtures/tasks/tasks.json
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 141,
|
||||||
|
"type": "file",
|
||||||
|
"result": "sample 2.pdf: Not consuming sample 2.pdf: It is a duplicate. : Traceback (most recent call last):\n File \"/Users/admin/.local/share/virtualenvs/paperless-ngx.nosync-udqDZzaE/lib/python3.8/site-packages/django_q/cluster.py\", line 432, in worker\n res = f(*task[\"args\"], **task[\"kwargs\"])\n File \"/Users/admin/Documents/paperless-ngx/src/documents/tasks.py\", line 316, in consume_file\n document = Consumer().try_consume_file(\n File \"/Users/admin/Documents/paperless-ngx/src/documents/consumer.py\", line 218, in try_consume_file\n self.pre_check_duplicate()\n File \"/Users/admin/Documents/paperless-ngx/src/documents/consumer.py\", line 113, in pre_check_duplicate\n self._fail(\n File \"/Users/admin/Documents/paperless-ngx/src/documents/consumer.py\", line 84, in _fail\n raise ConsumerError(f\"{self.filename}: {log_message or message}\")\ndocuments.consumer.ConsumerError: sample 2.pdf: Not consuming sample 2.pdf: It is a duplicate.\n",
|
||||||
|
"status": "FAILURE",
|
||||||
|
"task_id": "d8ddbe298a42427d82553206ddf0bc94",
|
||||||
|
"task_file_name": "sample 2.pdf",
|
||||||
|
"date_created": "2022-05-26T23:17:38.333474-07:00",
|
||||||
|
"date_done": null,
|
||||||
|
"acknowledged": false,
|
||||||
|
"related_document": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 132,
|
||||||
|
"type": "file",
|
||||||
|
"result": " : Traceback (most recent call last):\n File \"/Users/admin/.local/share/virtualenvs/paperless-ng/lib/python3.6/site-packages/ocrmypdf/subprocess.py\", line 131, in get_version\n env=env,\n File \"/Users/admin/.local/share/virtualenvs/paperless-ng/lib/python3.6/site-packages/ocrmypdf/subprocess.py\", line 68, in run\n proc = subprocess_run(args, env=env, **kwargs)\n File \"/Users/admin/opt/anaconda3/envs/paperless-ng/lib/python3.6/subprocess.py\", line 423, in run\n with Popen(*popenargs, **kwargs) as process:\n File \"/Users/admin/opt/anaconda3/envs/paperless-ng/lib/python3.6/subprocess.py\", line 729, in __init__\n restore_signals, start_new_session)\n File \"/Users/admin/opt/anaconda3/envs/paperless-ng/lib/python3.6/subprocess.py\", line 1364, in _execute_child\n raise child_exception_type(errno_num, err_msg, err_filename)\nFileNotFoundError: [Errno 2] No such file or directory: 'unpaper': 'unpaper'\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/Users/admin/.local/share/virtualenvs/paperless-ng/lib/python3.6/site-packages/ocrmypdf/subprocess.py\", line 287, in check_external_program\n found_version = version_checker()\n File \"/Users/admin/.local/share/virtualenvs/paperless-ng/lib/python3.6/site-packages/ocrmypdf/_exec/unpaper.py\", line 34, in version\n return get_version('unpaper')\n File \"/Users/admin/.local/share/virtualenvs/paperless-ng/lib/python3.6/site-packages/ocrmypdf/subprocess.py\", line 137, in get_version\n ) from e\nocrmypdf.exceptions.MissingDependencyError: Could not find program 'unpaper' on the PATH\n\nDuring handling of the above exception, another exception occurred:\n\nTraceback (most recent call last):\n File \"/Users/admin/Documents/Work/Contributions/paperless-ng/src/paperless_tesseract/parsers.py\", line 176, in parse\n ocrmypdf.ocr(**ocr_args)\n File \"/Users/admin/.local/share/virtualenvs/paperless-ng/lib/python3.6/site-packages/ocrmypdf/api.py\", line 315, in ocr\n check_options(options, plugin_manager)\n File \"/Users/admin/.local/share/virtualenvs/paperless-ng/lib/python3.6/site-packages/ocrmypdf/_validation.py\", line 260, in check_options\n _check_options(options, plugin_manager, ocr_engine_languages)\n File \"/Users/admin/.local/share/virtualenvs/paperless-ng/lib/python3.6/site-packages/ocrmypdf/_validation.py\", line 250, in _check_options\n check_options_preprocessing(options)\n File \"/Users/admin/.local/share/virtualenvs/paperless-ng/lib/python3.6/site-packages/ocrmypdf/_validation.py\", line 128, in check_options_preprocessing\n required_for=['--clean, --clean-final'],\n File \"/Users/admin/.local/share/virtualenvs/paperless-ng/lib/python3.6/site-packages/ocrmypdf/subprocess.py\", line 293, in check_external_program\n raise MissingDependencyError()\nocrmypdf.exceptions.MissingDependencyError\n\nDuring handling of the above exception, another exception occurred:\n\nTraceback (most recent call last):\n File \"/Users/admin/Documents/Work/Contributions/paperless-ng/src/documents/consumer.py\", line 179, in try_consume_file\n document_parser.parse(self.path, mime_type, self.filename)\n File \"/Users/admin/Documents/Work/Contributions/paperless-ng/src/paperless_tesseract/parsers.py\", line 197, in parse\n raise ParseError(e)\ndocuments.parsers.ParseError\n\nDuring handling of the above exception, another exception occurred:\n\nTraceback (most recent call last):\n File \"/Users/admin/.local/share/virtualenvs/paperless-ng/lib/python3.6/site-packages/django_q/cluster.py\", line 436, in worker\n res = f(*task[\"args\"], **task[\"kwargs\"])\n File \"/Users/admin/Documents/Work/Contributions/paperless-ng/src/documents/tasks.py\", line 73, in consume_file\n override_tag_ids=override_tag_ids)\n File \"/Users/admin/Documents/Work/Contributions/paperless-ng/src/documents/consumer.py\", line 196, in try_consume_file\n raise ConsumerError(e)\ndocuments.consumer.ConsumerError\n",
|
||||||
|
"status": "FAILURE",
|
||||||
|
"task_id": "4c554075552c4cc985abd76e6f274c90",
|
||||||
|
"task_file_name": "pdf-sample 10.24.48 PM.pdf",
|
||||||
|
"date_created": "2022-05-26T14:26:07.846365-07:00",
|
||||||
|
"date_done": null,
|
||||||
|
"acknowledged": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 115,
|
||||||
|
"type": "file",
|
||||||
|
"result": "2021-01-24 2021-01-20 sample_wide_orange.pdf: Document is a duplicate : Traceback (most recent call last):\n File \"/Users/admin/.local/share/virtualenvs/paperless-ng/lib/python3.6/site-packages/django_q/cluster.py\", line 436, in worker\n res = f(*task[\"args\"], **task[\"kwargs\"])\n File \"/Users/admin/Documents/Work/Contributions/paperless-ng/src/documents/tasks.py\", line 75, in consume_file\n task_id=task_id\n File \"/Users/admin/Documents/Work/Contributions/paperless-ng/src/documents/consumer.py\", line 168, in try_consume_file\n self.pre_check_duplicate()\n File \"/Users/admin/Documents/Work/Contributions/paperless-ng/src/documents/consumer.py\", line 85, in pre_check_duplicate\n self._fail(\"Document is a duplicate\")\n File \"/Users/admin/Documents/Work/Contributions/paperless-ng/src/documents/consumer.py\", line 53, in _fail\n raise ConsumerError(f\"{self.filename}: {message}\")\ndocuments.consumer.ConsumerError: 2021-01-24 2021-01-20 sample_wide_orange.pdf: Document is a duplicate\n",
|
||||||
|
"status": "FAILURE",
|
||||||
|
"task_id": "86494713646a4364b01da17aadca071d",
|
||||||
|
"task_file_name": "2021-01-24 2021-01-20 sample_wide_orange.pdf",
|
||||||
|
"date_created": "2022-05-26T14:26:07.817608-07:00",
|
||||||
|
"date_done": null,
|
||||||
|
"acknowledged": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 85,
|
||||||
|
"type": "file",
|
||||||
|
"result": "cannot open resource : Traceback (most recent call last):\n File \"/Users/admin/.local/share/virtualenvs/paperless-ng/lib/python3.6/site-packages/django_q/cluster.py\", line 436, in worker\n res = f(*task[\"args\"], **task[\"kwargs\"])\n File \"/Users/admin/Documents/Work/Contributions/paperless-ng/src/documents/tasks.py\", line 81, in consume_file\n task_id=task_id\n File \"/Users/admin/Documents/Work/Contributions/paperless-ng/src/documents/consumer.py\", line 244, in try_consume_file\n self.path, mime_type, self.filename)\n File \"/Users/admin/Documents/Work/Contributions/paperless-ng/src/documents/parsers.py\", line 302, in get_optimised_thumbnail\n thumbnail = self.get_thumbnail(document_path, mime_type, file_name)\n File \"/Users/admin/Documents/Work/Contributions/paperless-ng/src/paperless_text/parsers.py\", line 29, in get_thumbnail\n layout_engine=ImageFont.LAYOUT_BASIC)\n File \"/Users/admin/.local/share/virtualenvs/paperless-ng/lib/python3.6/site-packages/PIL/ImageFont.py\", line 852, in truetype\n return freetype(font)\n File \"/Users/admin/.local/share/virtualenvs/paperless-ng/lib/python3.6/site-packages/PIL/ImageFont.py\", line 849, in freetype\n return FreeTypeFont(font, size, index, encoding, layout_engine)\n File \"/Users/admin/.local/share/virtualenvs/paperless-ng/lib/python3.6/site-packages/PIL/ImageFont.py\", line 210, in __init__\n font, size, index, encoding, layout_engine=layout_engine\nOSError: cannot open resource\n",
|
||||||
|
"status": "FAILURE",
|
||||||
|
"task_id": "abca803fa46342e1ac81f3d3f2080e79",
|
||||||
|
"task_file_name": "simple.txt",
|
||||||
|
"date_created": "2022-05-26T14:26:07.771541-07:00",
|
||||||
|
"date_done": null,
|
||||||
|
"acknowledged": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 41,
|
||||||
|
"type": "file",
|
||||||
|
"result": "commands.txt: Not consuming commands.txt: It is a duplicate. : Traceback (most recent call last):\n File \"/Users/admin/.local/share/virtualenvs/paperless-ngx.nosync-udqDZzaE/lib/python3.8/site-packages/django_q/cluster.py\", line 432, in worker\n res = f(*task[\"args\"], **task[\"kwargs\"])\n File \"/Users/admin/Documents/paperless-ngx/src/documents/tasks.py\", line 70, in consume_file\n document = Consumer().try_consume_file(\n File \"/Users/admin/Documents/paperless-ngx/src/documents/consumer.py\", line 199, in try_consume_file\n self.pre_check_duplicate()\n File \"/Users/admin/Documents/paperless-ngx/src/documents/consumer.py\", line 97, in pre_check_duplicate\n self._fail(\n File \"/Users/admin/Documents/paperless-ngx/src/documents/consumer.py\", line 69, in _fail\n raise ConsumerError(f\"{self.filename}: {log_message or message}\")\ndocuments.consumer.ConsumerError: commands.txt: Not consuming commands.txt: It is a duplicate.\n",
|
||||||
|
"status": "FAILURE",
|
||||||
|
"task_id": "0af67672e8e14404b060d4cf8f69313d",
|
||||||
|
"task_file_name": "commands.txt",
|
||||||
|
"date_created": "2022-05-26T14:26:07.704247-07:00",
|
||||||
|
"date_done": null,
|
||||||
|
"acknowledged": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"type": "file",
|
||||||
|
"result": "Success. New document id 260 created",
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"task_id": "b7629a0f41bd40c7a3ea4680341321b5",
|
||||||
|
"task_file_name": "2022-03-24+Sonstige+ScanPC2022-03-24_081058.pdf",
|
||||||
|
"date_created": "2022-05-26T14:26:07.670577-07:00",
|
||||||
|
"date_done": "2022-05-26T14:26:07.670577-07:00",
|
||||||
|
"acknowledged": false,
|
||||||
|
"related_document": 260
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"type": "file",
|
||||||
|
"result": "Success. New document id 261 created",
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"task_id": "02e276a86a424ccfb83309df5d8594be",
|
||||||
|
"task_file_name": "2sample-pdf-with-images.pdf",
|
||||||
|
"date_created": "2022-05-26T14:26:07.668987-07:00",
|
||||||
|
"date_done": "2022-05-26T14:26:07.668987-07:00",
|
||||||
|
"acknowledged": false,
|
||||||
|
"related_document": 261
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"type": "file",
|
||||||
|
"result": "Success. New document id 262 created",
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"task_id": "41229b8be9b445c0a523697d0f58f13e",
|
||||||
|
"task_file_name": "2sample-pdf-with-images_pw.pdf",
|
||||||
|
"date_created": "2022-05-26T14:26:07.667993-07:00",
|
||||||
|
"date_done": "2022-05-26T14:26:07.667993-07:00",
|
||||||
|
"acknowledged": false,
|
||||||
|
"related_document": 262
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"type": "file",
|
||||||
|
"result": "Success. New document id 264 created",
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"task_id": "bbbca32d408c4619bd0b512a8327c773",
|
||||||
|
"task_file_name": "homebridge.log",
|
||||||
|
"date_created": "2022-05-26T14:26:07.665560-07:00",
|
||||||
|
"date_done": "2022-05-26T14:26:07.665560-07:00",
|
||||||
|
"acknowledged": false,
|
||||||
|
"related_document": 264
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type": "file",
|
||||||
|
"result": "Success. New document id 265 created",
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"task_id": "00ab285ab4bf482ab30c7d580b252ecb",
|
||||||
|
"task_file_name": "IMG_7459.PNG",
|
||||||
|
"date_created": "2022-05-26T14:26:07.664506-07:00",
|
||||||
|
"date_done": "2022-05-26T14:26:07.664506-07:00",
|
||||||
|
"acknowledged": false,
|
||||||
|
"related_document": 265
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"type": "file",
|
||||||
|
"result": "Success. New document id 267 created",
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"task_id": "289c5163cfec410db42948a0cacbeb9c",
|
||||||
|
"task_file_name": "IMG_7459.PNG",
|
||||||
|
"date_created": "2022-05-26T14:26:07.659661-07:00",
|
||||||
|
"date_done": "2022-05-26T14:26:07.659661-07:00",
|
||||||
|
"acknowledged": false,
|
||||||
|
"related_document": 267
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "file",
|
||||||
|
"result": null,
|
||||||
|
"status": "STARTED",
|
||||||
|
"task_id": "7a4ebdb2bde04311935284027ef8ca65",
|
||||||
|
"task_file_name": "2019-08-04 DSA Questionnaire - 5-8-19.pdf",
|
||||||
|
"date_created": "2022-05-26T14:26:07.655276-07:00",
|
||||||
|
"date_done": null,
|
||||||
|
"acknowledged": false,
|
||||||
|
"related_document": null
|
||||||
|
}
|
||||||
|
]
|
163
src-ui/cypress/fixtures/ui_settings/settings.json
Normal file
163
src-ui/cypress/fixtures/ui_settings/settings.json
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
{
|
||||||
|
"user": {
|
||||||
|
"id": 1,
|
||||||
|
"username": "admin",
|
||||||
|
"is_superuser": true,
|
||||||
|
"groups": []
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"language": "",
|
||||||
|
"bulk_edit": {
|
||||||
|
"confirmation_dialogs": true,
|
||||||
|
"apply_on_close": false
|
||||||
|
},
|
||||||
|
"documentListSize": 50,
|
||||||
|
"dark_mode": {
|
||||||
|
"use_system": true,
|
||||||
|
"enabled": "false",
|
||||||
|
"thumb_inverted": "true"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"color": "#b198e5"
|
||||||
|
},
|
||||||
|
"document_details": {
|
||||||
|
"native_pdf_viewer": false
|
||||||
|
},
|
||||||
|
"date_display": {
|
||||||
|
"date_locale": "",
|
||||||
|
"date_format": "mediumDate"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"consumer_new_documents": true,
|
||||||
|
"consumer_success": true,
|
||||||
|
"consumer_failed": true,
|
||||||
|
"consumer_suppress_on_dashboard": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"permissions": [
|
||||||
|
"add_logentry",
|
||||||
|
"change_logentry",
|
||||||
|
"delete_logentry",
|
||||||
|
"view_logentry",
|
||||||
|
"add_group",
|
||||||
|
"change_group",
|
||||||
|
"delete_group",
|
||||||
|
"view_group",
|
||||||
|
"add_permission",
|
||||||
|
"change_permission",
|
||||||
|
"delete_permission",
|
||||||
|
"view_permission",
|
||||||
|
"add_user",
|
||||||
|
"change_user",
|
||||||
|
"delete_user",
|
||||||
|
"view_user",
|
||||||
|
"add_token",
|
||||||
|
"change_token",
|
||||||
|
"delete_token",
|
||||||
|
"view_token",
|
||||||
|
"add_tokenproxy",
|
||||||
|
"change_tokenproxy",
|
||||||
|
"delete_tokenproxy",
|
||||||
|
"view_tokenproxy",
|
||||||
|
"add_contenttype",
|
||||||
|
"change_contenttype",
|
||||||
|
"delete_contenttype",
|
||||||
|
"view_contenttype",
|
||||||
|
"add_chordcounter",
|
||||||
|
"change_chordcounter",
|
||||||
|
"delete_chordcounter",
|
||||||
|
"view_chordcounter",
|
||||||
|
"add_groupresult",
|
||||||
|
"change_groupresult",
|
||||||
|
"delete_groupresult",
|
||||||
|
"view_groupresult",
|
||||||
|
"add_taskresult",
|
||||||
|
"change_taskresult",
|
||||||
|
"delete_taskresult",
|
||||||
|
"view_taskresult",
|
||||||
|
"add_failure",
|
||||||
|
"change_failure",
|
||||||
|
"delete_failure",
|
||||||
|
"view_failure",
|
||||||
|
"add_ormq",
|
||||||
|
"change_ormq",
|
||||||
|
"delete_ormq",
|
||||||
|
"view_ormq",
|
||||||
|
"add_schedule",
|
||||||
|
"change_schedule",
|
||||||
|
"delete_schedule",
|
||||||
|
"view_schedule",
|
||||||
|
"add_success",
|
||||||
|
"change_success",
|
||||||
|
"delete_success",
|
||||||
|
"view_success",
|
||||||
|
"add_task",
|
||||||
|
"change_task",
|
||||||
|
"delete_task",
|
||||||
|
"view_task",
|
||||||
|
"add_note",
|
||||||
|
"change_note",
|
||||||
|
"delete_note",
|
||||||
|
"view_note",
|
||||||
|
"add_correspondent",
|
||||||
|
"change_correspondent",
|
||||||
|
"delete_correspondent",
|
||||||
|
"view_correspondent",
|
||||||
|
"add_document",
|
||||||
|
"change_document",
|
||||||
|
"delete_document",
|
||||||
|
"view_document",
|
||||||
|
"add_documenttype",
|
||||||
|
"change_documenttype",
|
||||||
|
"delete_documenttype",
|
||||||
|
"view_documenttype",
|
||||||
|
"add_frontendsettings",
|
||||||
|
"change_frontendsettings",
|
||||||
|
"delete_frontendsettings",
|
||||||
|
"view_frontendsettings",
|
||||||
|
"add_log",
|
||||||
|
"change_log",
|
||||||
|
"delete_log",
|
||||||
|
"view_log",
|
||||||
|
"add_paperlesstask",
|
||||||
|
"change_paperlesstask",
|
||||||
|
"delete_paperlesstask",
|
||||||
|
"view_paperlesstask",
|
||||||
|
"add_savedview",
|
||||||
|
"change_savedview",
|
||||||
|
"delete_savedview",
|
||||||
|
"view_savedview",
|
||||||
|
"add_savedviewfilterrule",
|
||||||
|
"change_savedviewfilterrule",
|
||||||
|
"delete_savedviewfilterrule",
|
||||||
|
"view_savedviewfilterrule",
|
||||||
|
"add_storagepath",
|
||||||
|
"change_storagepath",
|
||||||
|
"delete_storagepath",
|
||||||
|
"view_storagepath",
|
||||||
|
"add_tag",
|
||||||
|
"change_tag",
|
||||||
|
"delete_tag",
|
||||||
|
"view_tag",
|
||||||
|
"add_taskattributes",
|
||||||
|
"change_taskattributes",
|
||||||
|
"delete_taskattributes",
|
||||||
|
"view_taskattributes",
|
||||||
|
"add_uisettings",
|
||||||
|
"change_uisettings",
|
||||||
|
"delete_uisettings",
|
||||||
|
"view_uisettings",
|
||||||
|
"add_mailaccount",
|
||||||
|
"change_mailaccount",
|
||||||
|
"delete_mailaccount",
|
||||||
|
"view_mailaccount",
|
||||||
|
"add_mailrule",
|
||||||
|
"change_mailrule",
|
||||||
|
"delete_mailrule",
|
||||||
|
"view_mailrule",
|
||||||
|
"add_session",
|
||||||
|
"change_session",
|
||||||
|
"delete_session",
|
||||||
|
"view_session"
|
||||||
|
]
|
||||||
|
}
|
88
src-ui/cypress/fixtures/ui_settings/settings_restricted.json
Normal file
88
src-ui/cypress/fixtures/ui_settings/settings_restricted.json
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
{
|
||||||
|
"user": {
|
||||||
|
"id": 1,
|
||||||
|
"username": "admin",
|
||||||
|
"is_superuser": false,
|
||||||
|
"groups": []
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"language": "",
|
||||||
|
"bulk_edit": {
|
||||||
|
"confirmation_dialogs": true,
|
||||||
|
"apply_on_close": false
|
||||||
|
},
|
||||||
|
"documentListSize": 50,
|
||||||
|
"dark_mode": {
|
||||||
|
"use_system": true,
|
||||||
|
"enabled": "false",
|
||||||
|
"thumb_inverted": "true"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"color": "#b198e5"
|
||||||
|
},
|
||||||
|
"document_details": {
|
||||||
|
"native_pdf_viewer": false
|
||||||
|
},
|
||||||
|
"date_display": {
|
||||||
|
"date_locale": "",
|
||||||
|
"date_format": "mediumDate"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"consumer_new_documents": true,
|
||||||
|
"consumer_success": true,
|
||||||
|
"consumer_failed": true,
|
||||||
|
"consumer_suppress_on_dashboard": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"permissions": [
|
||||||
|
"add_token",
|
||||||
|
"change_token",
|
||||||
|
"delete_token",
|
||||||
|
"view_token",
|
||||||
|
"add_tokenproxy",
|
||||||
|
"change_tokenproxy",
|
||||||
|
"delete_tokenproxy",
|
||||||
|
"view_tokenproxy",
|
||||||
|
"add_contenttype",
|
||||||
|
"change_contenttype",
|
||||||
|
"delete_contenttype",
|
||||||
|
"view_contenttype",
|
||||||
|
"add_chordcounter",
|
||||||
|
"change_chordcounter",
|
||||||
|
"delete_chordcounter",
|
||||||
|
"view_chordcounter",
|
||||||
|
"add_groupresult",
|
||||||
|
"change_groupresult",
|
||||||
|
"delete_groupresult",
|
||||||
|
"view_groupresult",
|
||||||
|
"add_failure",
|
||||||
|
"change_failure",
|
||||||
|
"delete_failure",
|
||||||
|
"view_failure",
|
||||||
|
"add_ormq",
|
||||||
|
"change_ormq",
|
||||||
|
"delete_ormq",
|
||||||
|
"view_ormq",
|
||||||
|
"add_schedule",
|
||||||
|
"change_schedule",
|
||||||
|
"delete_schedule",
|
||||||
|
"view_schedule",
|
||||||
|
"add_success",
|
||||||
|
"change_success",
|
||||||
|
"delete_success",
|
||||||
|
"view_success",
|
||||||
|
"add_task",
|
||||||
|
"change_task",
|
||||||
|
"delete_task",
|
||||||
|
"view_task",
|
||||||
|
"add_note",
|
||||||
|
"add_frontendsettings",
|
||||||
|
"change_frontendsettings",
|
||||||
|
"delete_frontendsettings",
|
||||||
|
"view_frontendsettings",
|
||||||
|
"add_session",
|
||||||
|
"change_session",
|
||||||
|
"delete_session",
|
||||||
|
"view_session"
|
||||||
|
]
|
||||||
|
}
|
459
src-ui/cypress/fixtures/users/users.json
Normal file
459
src-ui/cypress/fixtures/users/users.json
Normal file
@ -0,0 +1,459 @@
|
|||||||
|
{
|
||||||
|
"count": 4,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"username": "admin",
|
||||||
|
"password": "**********",
|
||||||
|
"first_name": "",
|
||||||
|
"last_name": "",
|
||||||
|
"date_joined": "2022-02-14T23:11:09.103293Z",
|
||||||
|
"is_staff": true,
|
||||||
|
"is_active": true,
|
||||||
|
"is_superuser": true,
|
||||||
|
"groups": [],
|
||||||
|
"user_permissions": [],
|
||||||
|
"inherited_permissions": [
|
||||||
|
"auth.delete_permission",
|
||||||
|
"paperless_mail.change_mailrule",
|
||||||
|
"django_celery_results.add_taskresult",
|
||||||
|
"documents.view_taskattributes",
|
||||||
|
"documents.view_paperlesstask",
|
||||||
|
"django_q.add_success",
|
||||||
|
"documents.view_uisettings",
|
||||||
|
"auth.change_user",
|
||||||
|
"admin.delete_logentry",
|
||||||
|
"django_celery_results.change_taskresult",
|
||||||
|
"django_q.change_schedule",
|
||||||
|
"django_celery_results.delete_taskresult",
|
||||||
|
"paperless_mail.add_mailaccount",
|
||||||
|
"auth.change_group",
|
||||||
|
"documents.add_note",
|
||||||
|
"paperless_mail.delete_mailaccount",
|
||||||
|
"authtoken.delete_tokenproxy",
|
||||||
|
"guardian.delete_groupobjectpermission",
|
||||||
|
"contenttypes.delete_contenttype",
|
||||||
|
"documents.change_correspondent",
|
||||||
|
"authtoken.delete_token",
|
||||||
|
"documents.delete_documenttype",
|
||||||
|
"django_q.change_ormq",
|
||||||
|
"documents.change_savedviewfilterrule",
|
||||||
|
"auth.delete_group",
|
||||||
|
"documents.add_documenttype",
|
||||||
|
"django_q.change_success",
|
||||||
|
"documents.delete_tag",
|
||||||
|
"documents.change_note",
|
||||||
|
"django_q.delete_task",
|
||||||
|
"documents.add_savedviewfilterrule",
|
||||||
|
"django_q.view_task",
|
||||||
|
"paperless_mail.add_mailrule",
|
||||||
|
"paperless_mail.view_mailaccount",
|
||||||
|
"documents.add_frontendsettings",
|
||||||
|
"sessions.change_session",
|
||||||
|
"documents.view_savedview",
|
||||||
|
"authtoken.add_tokenproxy",
|
||||||
|
"documents.change_tag",
|
||||||
|
"documents.view_document",
|
||||||
|
"documents.add_savedview",
|
||||||
|
"auth.delete_user",
|
||||||
|
"documents.view_log",
|
||||||
|
"documents.view_note",
|
||||||
|
"guardian.change_groupobjectpermission",
|
||||||
|
"sessions.delete_session",
|
||||||
|
"django_q.change_failure",
|
||||||
|
"guardian.change_userobjectpermission",
|
||||||
|
"documents.change_storagepath",
|
||||||
|
"documents.delete_document",
|
||||||
|
"documents.delete_taskattributes",
|
||||||
|
"django_celery_results.change_groupresult",
|
||||||
|
"django_q.add_ormq",
|
||||||
|
"guardian.view_groupobjectpermission",
|
||||||
|
"admin.change_logentry",
|
||||||
|
"django_q.delete_schedule",
|
||||||
|
"documents.delete_paperlesstask",
|
||||||
|
"django_q.view_ormq",
|
||||||
|
"documents.change_paperlesstask",
|
||||||
|
"guardian.delete_userobjectpermission",
|
||||||
|
"auth.view_permission",
|
||||||
|
"auth.view_user",
|
||||||
|
"django_q.add_schedule",
|
||||||
|
"authtoken.change_token",
|
||||||
|
"guardian.add_groupobjectpermission",
|
||||||
|
"documents.view_documenttype",
|
||||||
|
"documents.change_log",
|
||||||
|
"paperless_mail.delete_mailrule",
|
||||||
|
"auth.view_group",
|
||||||
|
"authtoken.view_token",
|
||||||
|
"admin.view_logentry",
|
||||||
|
"django_celery_results.view_chordcounter",
|
||||||
|
"django_celery_results.view_groupresult",
|
||||||
|
"documents.view_storagepath",
|
||||||
|
"documents.add_storagepath",
|
||||||
|
"django_celery_results.add_groupresult",
|
||||||
|
"documents.view_tag",
|
||||||
|
"guardian.view_userobjectpermission",
|
||||||
|
"documents.delete_correspondent",
|
||||||
|
"documents.add_tag",
|
||||||
|
"documents.delete_savedviewfilterrule",
|
||||||
|
"documents.add_correspondent",
|
||||||
|
"authtoken.view_tokenproxy",
|
||||||
|
"documents.delete_frontendsettings",
|
||||||
|
"django_celery_results.delete_chordcounter",
|
||||||
|
"django_q.change_task",
|
||||||
|
"documents.add_taskattributes",
|
||||||
|
"documents.delete_storagepath",
|
||||||
|
"sessions.add_session",
|
||||||
|
"documents.add_uisettings",
|
||||||
|
"documents.change_taskattributes",
|
||||||
|
"documents.delete_uisettings",
|
||||||
|
"django_q.delete_ormq",
|
||||||
|
"auth.change_permission",
|
||||||
|
"documents.view_savedviewfilterrule",
|
||||||
|
"documents.change_frontendsettings",
|
||||||
|
"documents.change_documenttype",
|
||||||
|
"documents.view_correspondent",
|
||||||
|
"auth.add_user",
|
||||||
|
"paperless_mail.change_mailaccount",
|
||||||
|
"documents.add_paperlesstask",
|
||||||
|
"django_q.view_success",
|
||||||
|
"django_celery_results.delete_groupresult",
|
||||||
|
"documents.delete_savedview",
|
||||||
|
"authtoken.change_tokenproxy",
|
||||||
|
"documents.view_frontendsettings",
|
||||||
|
"authtoken.add_token",
|
||||||
|
"django_celery_results.add_chordcounter",
|
||||||
|
"contenttypes.change_contenttype",
|
||||||
|
"admin.add_logentry",
|
||||||
|
"django_q.delete_failure",
|
||||||
|
"documents.change_uisettings",
|
||||||
|
"django_q.view_failure",
|
||||||
|
"documents.add_log",
|
||||||
|
"documents.change_savedview",
|
||||||
|
"paperless_mail.view_mailrule",
|
||||||
|
"django_q.view_schedule",
|
||||||
|
"documents.change_document",
|
||||||
|
"django_celery_results.change_chordcounter",
|
||||||
|
"documents.add_document",
|
||||||
|
"django_celery_results.view_taskresult",
|
||||||
|
"contenttypes.add_contenttype",
|
||||||
|
"django_q.delete_success",
|
||||||
|
"documents.delete_note",
|
||||||
|
"django_q.add_failure",
|
||||||
|
"guardian.add_userobjectpermission",
|
||||||
|
"sessions.view_session",
|
||||||
|
"contenttypes.view_contenttype",
|
||||||
|
"auth.add_permission",
|
||||||
|
"documents.delete_log",
|
||||||
|
"django_q.add_task",
|
||||||
|
"auth.add_group"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 15,
|
||||||
|
"username": "test",
|
||||||
|
"password": "**********",
|
||||||
|
"first_name": "",
|
||||||
|
"last_name": "",
|
||||||
|
"date_joined": "2022-11-23T08:30:54Z",
|
||||||
|
"is_staff": true,
|
||||||
|
"is_active": true,
|
||||||
|
"is_superuser": false,
|
||||||
|
"groups": [
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"user_permissions": [
|
||||||
|
"add_group",
|
||||||
|
"change_group",
|
||||||
|
"delete_group",
|
||||||
|
"view_group",
|
||||||
|
"add_permission",
|
||||||
|
"change_permission",
|
||||||
|
"delete_permission",
|
||||||
|
"view_permission",
|
||||||
|
"add_token",
|
||||||
|
"change_token",
|
||||||
|
"delete_token",
|
||||||
|
"view_token",
|
||||||
|
"add_tokenproxy",
|
||||||
|
"change_tokenproxy",
|
||||||
|
"delete_tokenproxy",
|
||||||
|
"view_tokenproxy",
|
||||||
|
"add_contenttype",
|
||||||
|
"change_contenttype",
|
||||||
|
"delete_contenttype",
|
||||||
|
"view_contenttype",
|
||||||
|
"add_chordcounter",
|
||||||
|
"change_chordcounter",
|
||||||
|
"delete_chordcounter",
|
||||||
|
"view_chordcounter",
|
||||||
|
"add_groupresult",
|
||||||
|
"change_groupresult",
|
||||||
|
"delete_groupresult",
|
||||||
|
"view_groupresult",
|
||||||
|
"add_taskresult",
|
||||||
|
"change_taskresult",
|
||||||
|
"delete_taskresult",
|
||||||
|
"view_taskresult",
|
||||||
|
"add_failure",
|
||||||
|
"change_failure",
|
||||||
|
"delete_failure",
|
||||||
|
"view_failure",
|
||||||
|
"add_ormq",
|
||||||
|
"change_ormq",
|
||||||
|
"delete_ormq",
|
||||||
|
"view_ormq",
|
||||||
|
"add_schedule",
|
||||||
|
"change_schedule",
|
||||||
|
"delete_schedule",
|
||||||
|
"view_schedule",
|
||||||
|
"add_success",
|
||||||
|
"change_success",
|
||||||
|
"delete_success",
|
||||||
|
"view_success",
|
||||||
|
"add_task",
|
||||||
|
"change_task",
|
||||||
|
"delete_task",
|
||||||
|
"view_task",
|
||||||
|
"add_note",
|
||||||
|
"change_note",
|
||||||
|
"delete_note",
|
||||||
|
"view_note",
|
||||||
|
"add_frontendsettings",
|
||||||
|
"change_frontendsettings",
|
||||||
|
"delete_frontendsettings",
|
||||||
|
"view_frontendsettings",
|
||||||
|
"add_log",
|
||||||
|
"change_log",
|
||||||
|
"delete_log",
|
||||||
|
"view_log",
|
||||||
|
"add_savedviewfilterrule",
|
||||||
|
"change_savedviewfilterrule",
|
||||||
|
"delete_savedviewfilterrule",
|
||||||
|
"view_savedviewfilterrule",
|
||||||
|
"add_taskattributes",
|
||||||
|
"change_taskattributes",
|
||||||
|
"delete_taskattributes",
|
||||||
|
"view_taskattributes",
|
||||||
|
"add_session",
|
||||||
|
"change_session",
|
||||||
|
"delete_session",
|
||||||
|
"view_session"
|
||||||
|
],
|
||||||
|
"inherited_permissions": [
|
||||||
|
"auth.delete_permission",
|
||||||
|
"django_celery_results.add_taskresult",
|
||||||
|
"documents.view_taskattributes",
|
||||||
|
"django_q.add_ormq",
|
||||||
|
"django_q.add_success",
|
||||||
|
"django_q.delete_schedule",
|
||||||
|
"django_q.view_ormq",
|
||||||
|
"auth.view_permission",
|
||||||
|
"django_q.add_schedule",
|
||||||
|
"django_celery_results.change_taskresult",
|
||||||
|
"django_q.change_schedule",
|
||||||
|
"django_celery_results.delete_taskresult",
|
||||||
|
"authtoken.change_token",
|
||||||
|
"auth.change_group",
|
||||||
|
"documents.add_note",
|
||||||
|
"authtoken.delete_tokenproxy",
|
||||||
|
"documents.view_documenttype",
|
||||||
|
"contenttypes.delete_contenttype",
|
||||||
|
"documents.change_correspondent",
|
||||||
|
"authtoken.delete_token",
|
||||||
|
"documents.change_log",
|
||||||
|
"auth.view_group",
|
||||||
|
"authtoken.view_token",
|
||||||
|
"django_celery_results.view_chordcounter",
|
||||||
|
"django_celery_results.view_groupresult",
|
||||||
|
"documents.delete_documenttype",
|
||||||
|
"django_q.change_ormq",
|
||||||
|
"documents.change_savedviewfilterrule",
|
||||||
|
"django_celery_results.add_groupresult",
|
||||||
|
"auth.delete_group",
|
||||||
|
"documents.add_documenttype",
|
||||||
|
"django_q.change_success",
|
||||||
|
"auth.add_permission",
|
||||||
|
"documents.delete_correspondent",
|
||||||
|
"documents.delete_savedviewfilterrule",
|
||||||
|
"documents.add_correspondent",
|
||||||
|
"authtoken.view_tokenproxy",
|
||||||
|
"documents.delete_frontendsettings",
|
||||||
|
"django_celery_results.delete_chordcounter",
|
||||||
|
"documents.add_taskattributes",
|
||||||
|
"django_q.change_task",
|
||||||
|
"sessions.add_session",
|
||||||
|
"documents.change_taskattributes",
|
||||||
|
"documents.change_note",
|
||||||
|
"django_q.delete_task",
|
||||||
|
"django_q.delete_ormq",
|
||||||
|
"auth.change_permission",
|
||||||
|
"documents.add_savedviewfilterrule",
|
||||||
|
"django_q.view_task",
|
||||||
|
"documents.view_savedviewfilterrule",
|
||||||
|
"documents.change_frontendsettings",
|
||||||
|
"documents.change_documenttype",
|
||||||
|
"documents.view_correspondent",
|
||||||
|
"django_q.view_success",
|
||||||
|
"documents.add_frontendsettings",
|
||||||
|
"django_celery_results.delete_groupresult",
|
||||||
|
"documents.delete_savedview",
|
||||||
|
"authtoken.change_tokenproxy",
|
||||||
|
"documents.view_frontendsettings",
|
||||||
|
"authtoken.add_token",
|
||||||
|
"sessions.change_session",
|
||||||
|
"django_celery_results.add_chordcounter",
|
||||||
|
"documents.view_savedview",
|
||||||
|
"contenttypes.change_contenttype",
|
||||||
|
"django_q.delete_failure",
|
||||||
|
"authtoken.add_tokenproxy",
|
||||||
|
"documents.view_document",
|
||||||
|
"documents.add_savedview",
|
||||||
|
"django_q.view_failure",
|
||||||
|
"documents.view_note",
|
||||||
|
"documents.view_log",
|
||||||
|
"documents.add_log",
|
||||||
|
"documents.change_savedview",
|
||||||
|
"django_q.view_schedule",
|
||||||
|
"documents.change_document",
|
||||||
|
"django_celery_results.change_chordcounter",
|
||||||
|
"documents.add_document",
|
||||||
|
"sessions.delete_session",
|
||||||
|
"django_q.change_failure",
|
||||||
|
"django_celery_results.view_taskresult",
|
||||||
|
"contenttypes.add_contenttype",
|
||||||
|
"django_q.delete_success",
|
||||||
|
"documents.delete_note",
|
||||||
|
"django_q.add_failure",
|
||||||
|
"sessions.view_session",
|
||||||
|
"contenttypes.view_contenttype",
|
||||||
|
"documents.delete_taskattributes",
|
||||||
|
"documents.delete_document",
|
||||||
|
"documents.delete_log",
|
||||||
|
"django_q.add_task",
|
||||||
|
"django_celery_results.change_groupresult",
|
||||||
|
"auth.add_group"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"username": "testuser",
|
||||||
|
"password": "**********",
|
||||||
|
"first_name": "",
|
||||||
|
"last_name": "",
|
||||||
|
"date_joined": "2022-11-16T04:14:20.484914Z",
|
||||||
|
"is_staff": false,
|
||||||
|
"is_active": true,
|
||||||
|
"is_superuser": false,
|
||||||
|
"groups": [
|
||||||
|
1,
|
||||||
|
6
|
||||||
|
],
|
||||||
|
"user_permissions": [
|
||||||
|
"add_logentry",
|
||||||
|
"change_logentry",
|
||||||
|
"delete_logentry",
|
||||||
|
"view_logentry"
|
||||||
|
],
|
||||||
|
"inherited_permissions": [
|
||||||
|
"auth.delete_permission",
|
||||||
|
"django_celery_results.add_taskresult",
|
||||||
|
"documents.view_taskattributes",
|
||||||
|
"django_q.add_ormq",
|
||||||
|
"django_q.add_success",
|
||||||
|
"django_q.delete_schedule",
|
||||||
|
"django_q.view_ormq",
|
||||||
|
"auth.change_user",
|
||||||
|
"auth.view_permission",
|
||||||
|
"auth.view_user",
|
||||||
|
"django_q.add_schedule",
|
||||||
|
"django_celery_results.change_taskresult",
|
||||||
|
"django_q.change_schedule",
|
||||||
|
"django_celery_results.delete_taskresult",
|
||||||
|
"authtoken.change_token",
|
||||||
|
"auth.change_group",
|
||||||
|
"documents.add_note",
|
||||||
|
"authtoken.delete_tokenproxy",
|
||||||
|
"documents.view_documenttype",
|
||||||
|
"contenttypes.delete_contenttype",
|
||||||
|
"documents.change_correspondent",
|
||||||
|
"authtoken.delete_token",
|
||||||
|
"documents.change_log",
|
||||||
|
"auth.view_group",
|
||||||
|
"authtoken.view_token",
|
||||||
|
"django_celery_results.view_chordcounter",
|
||||||
|
"django_celery_results.view_groupresult",
|
||||||
|
"documents.delete_documenttype",
|
||||||
|
"django_q.change_ormq",
|
||||||
|
"documents.change_savedviewfilterrule",
|
||||||
|
"django_celery_results.add_groupresult",
|
||||||
|
"auth.delete_group",
|
||||||
|
"documents.add_documenttype",
|
||||||
|
"django_q.change_success",
|
||||||
|
"auth.add_permission",
|
||||||
|
"documents.delete_correspondent",
|
||||||
|
"documents.delete_savedviewfilterrule",
|
||||||
|
"documents.add_correspondent",
|
||||||
|
"authtoken.view_tokenproxy",
|
||||||
|
"documents.delete_frontendsettings",
|
||||||
|
"django_celery_results.delete_chordcounter",
|
||||||
|
"documents.add_taskattributes",
|
||||||
|
"django_q.change_task",
|
||||||
|
"sessions.add_session",
|
||||||
|
"documents.change_taskattributes",
|
||||||
|
"documents.change_note",
|
||||||
|
"django_q.delete_task",
|
||||||
|
"django_q.delete_ormq",
|
||||||
|
"auth.change_permission",
|
||||||
|
"documents.add_savedviewfilterrule",
|
||||||
|
"django_q.view_task",
|
||||||
|
"documents.view_savedviewfilterrule",
|
||||||
|
"documents.change_frontendsettings",
|
||||||
|
"documents.change_documenttype",
|
||||||
|
"documents.view_correspondent",
|
||||||
|
"auth.add_user",
|
||||||
|
"django_q.view_success",
|
||||||
|
"documents.add_frontendsettings",
|
||||||
|
"django_celery_results.delete_groupresult",
|
||||||
|
"documents.delete_savedview",
|
||||||
|
"authtoken.change_tokenproxy",
|
||||||
|
"documents.view_frontendsettings",
|
||||||
|
"authtoken.add_token",
|
||||||
|
"sessions.change_session",
|
||||||
|
"django_celery_results.add_chordcounter",
|
||||||
|
"documents.view_savedview",
|
||||||
|
"contenttypes.change_contenttype",
|
||||||
|
"django_q.delete_failure",
|
||||||
|
"authtoken.add_tokenproxy",
|
||||||
|
"documents.view_document",
|
||||||
|
"documents.add_savedview",
|
||||||
|
"django_q.view_failure",
|
||||||
|
"documents.view_note",
|
||||||
|
"documents.view_log",
|
||||||
|
"auth.delete_user",
|
||||||
|
"documents.add_log",
|
||||||
|
"documents.change_savedview",
|
||||||
|
"django_q.view_schedule",
|
||||||
|
"documents.change_document",
|
||||||
|
"django_celery_results.change_chordcounter",
|
||||||
|
"documents.add_document",
|
||||||
|
"sessions.delete_session",
|
||||||
|
"django_q.change_failure",
|
||||||
|
"django_celery_results.view_taskresult",
|
||||||
|
"contenttypes.add_contenttype",
|
||||||
|
"django_q.delete_success",
|
||||||
|
"documents.delete_note",
|
||||||
|
"django_q.add_failure",
|
||||||
|
"sessions.view_session",
|
||||||
|
"contenttypes.view_contenttype",
|
||||||
|
"documents.delete_taskattributes",
|
||||||
|
"documents.delete_document",
|
||||||
|
"documents.delete_log",
|
||||||
|
"django_q.add_task",
|
||||||
|
"django_celery_results.change_groupresult",
|
||||||
|
"auth.add_group"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
3
src-ui/cypress/plugins/index.ts
Normal file
3
src-ui/cypress/plugins/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// Plugins enable you to tap into, modify, or extend the internal behavior of Cypress
|
||||||
|
// For more info, visit https://on.cypress.io/plugins-api
|
||||||
|
module.exports = (on, config) => {}
|
43
src-ui/cypress/support/commands.ts
Normal file
43
src-ui/cypress/support/commands.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// ***********************************************
|
||||||
|
// This example namespace declaration will help
|
||||||
|
// with Intellisense and code completion in your
|
||||||
|
// IDE or Text Editor.
|
||||||
|
// ***********************************************
|
||||||
|
// declare namespace Cypress {
|
||||||
|
// interface Chainable<Subject = any> {
|
||||||
|
// customCommand(param: any): typeof customCommand;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// function customCommand(param: any): void {
|
||||||
|
// console.warn(param);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// NOTE: You can use it like so:
|
||||||
|
// Cypress.Commands.add('customCommand', customCommand);
|
||||||
|
//
|
||||||
|
// ***********************************************
|
||||||
|
// This example commands.js shows you how to
|
||||||
|
// create various custom commands and overwrite
|
||||||
|
// existing commands.
|
||||||
|
//
|
||||||
|
// For more comprehensive examples of custom
|
||||||
|
// commands please read more here:
|
||||||
|
// https://on.cypress.io/custom-commands
|
||||||
|
// ***********************************************
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a parent command --
|
||||||
|
// Cypress.Commands.add("login", (email, password) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a child command --
|
||||||
|
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a dual command --
|
||||||
|
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This will overwrite an existing command --
|
||||||
|
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
55
src-ui/cypress/support/e2e.ts
Normal file
55
src-ui/cypress/support/e2e.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// mock API methods
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.intercept('http://localhost:8000/api/ui_settings/', {
|
||||||
|
fixture: 'ui_settings/settings.json',
|
||||||
|
}).as('ui-settings')
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/users/*', {
|
||||||
|
fixture: 'users/users.json',
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/groups/*', {
|
||||||
|
fixture: 'groups/groups.json',
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/remote_version/', {
|
||||||
|
fixture: 'remote_version/remote_version.json',
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/saved_views/*', {
|
||||||
|
fixture: 'saved_views/savedviews.json',
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/tags/*', {
|
||||||
|
fixture: 'tags/tags.json',
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/correspondents/*', {
|
||||||
|
fixture: 'correspondents/correspondents.json',
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/document_types/*', {
|
||||||
|
fixture: 'document_types/doctypes.json',
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/storage_paths/*', {
|
||||||
|
fixture: 'storage_paths/storage_paths.json',
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/tasks/', {
|
||||||
|
fixture: 'tasks/tasks.json',
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/documents/1/metadata/', {
|
||||||
|
fixture: 'documents/1/metadata.json',
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/documents/1/suggestions/', {
|
||||||
|
fixture: 'documents/1/suggestions.json',
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/documents/1/thumb/', {
|
||||||
|
fixture: 'documents/lorem-ipsum.png',
|
||||||
|
})
|
||||||
|
})
|
8
src-ui/cypress/tsconfig.json
Normal file
8
src-ui/cypress/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"include": ["**/*.ts"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"sourceMap": false,
|
||||||
|
"types": ["cypress"]
|
||||||
|
}
|
||||||
|
}
|
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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
25
src-ui/e2e/src/app.e2e-spec.ts
Normal file
25
src-ui/e2e/src/app.e2e-spec.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
13
src-ui/e2e/src/app.po.ts
Normal file
13
src-ui/e2e/src/app.po.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
8
src-ui/jest.config.js
Normal file
8
src-ui/jest.config.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
module.exports = {
|
||||||
|
moduleNameMapper: {
|
||||||
|
'@core/(.*)': '<rootDir>/src/app/core/$1',
|
||||||
|
},
|
||||||
|
preset: 'jest-preset-angular',
|
||||||
|
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
|
||||||
|
testPathIgnorePatterns: ['/node_modules/', '/cypress/'],
|
||||||
|
}
|
5298
src-ui/messages.xlf
Normal file
5298
src-ui/messages.xlf
Normal file
File diff suppressed because it is too large
Load Diff
18796
src-ui/package-lock.json
generated
Normal file
18796
src-ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
68
src-ui/package.json
Normal file
68
src-ui/package.json
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"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",
|
||||||
|
"cy:run": "cypress run",
|
||||||
|
"e2e:ci": "concurrently 'npm run start' 'wait-on http-get://localhost:4200 && npm run cy:run' --kill-others --success first"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/common": "~15.2.7",
|
||||||
|
"@angular/compiler": "~15.2.7",
|
||||||
|
"@angular/core": "~15.2.7",
|
||||||
|
"@angular/forms": "~15.2.7",
|
||||||
|
"@angular/localize": "~15.2.7",
|
||||||
|
"@angular/platform-browser": "~15.2.7",
|
||||||
|
"@angular/platform-browser-dynamic": "~15.2.7",
|
||||||
|
"@angular/router": "~15.2.7",
|
||||||
|
"@ng-bootstrap/ng-bootstrap": "^14.1.0",
|
||||||
|
"@ng-select/ng-select": "^10.0.4",
|
||||||
|
"@ngneat/dirty-check-forms": "^3.0.3",
|
||||||
|
"@popperjs/core": "^2.11.7",
|
||||||
|
"bootstrap": "^5.2.3",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
|
"mime-names": "^1.0.0",
|
||||||
|
"ng2-pdf-viewer": "^9.1.5",
|
||||||
|
"ngx-color": "^8.0.3",
|
||||||
|
"ngx-cookie-service": "^15.0.0",
|
||||||
|
"ngx-file-drop": "^15.0.0",
|
||||||
|
"ngx-ui-tour-ng-bootstrap": "^12.6.0",
|
||||||
|
"rxjs": "^7.8.0",
|
||||||
|
"tslib": "^2.4.1",
|
||||||
|
"uuid": "^9.0.0",
|
||||||
|
"zone.js": "^0.13.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular-builders/jest": "15.0.0",
|
||||||
|
"@angular-devkit/build-angular": "~15.2.6",
|
||||||
|
"@angular-eslint/builder": "15.2.1",
|
||||||
|
"@angular-eslint/eslint-plugin": "15.2.1",
|
||||||
|
"@angular-eslint/eslint-plugin-template": "15.2.1",
|
||||||
|
"@angular-eslint/schematics": "15.2.1",
|
||||||
|
"@angular-eslint/template-parser": "15.2.1",
|
||||||
|
"@angular/cli": "~15.2.6",
|
||||||
|
"@angular/compiler-cli": "~15.2.7",
|
||||||
|
"@types/jest": "^29.5.0",
|
||||||
|
"@types/node": "^18.15.11",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.58.0",
|
||||||
|
"@typescript-eslint/parser": "^5.58.0",
|
||||||
|
"concurrently": "^8.0.1",
|
||||||
|
"eslint": "^8.38.0",
|
||||||
|
"jest": "28.1.3",
|
||||||
|
"jest-environment-jsdom": "^29.5.0",
|
||||||
|
"jest-preset-angular": "^12.2.6",
|
||||||
|
"ts-node": "~10.9.1",
|
||||||
|
"typescript": "~4.9.5",
|
||||||
|
"wait-on": "^7.0.1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@cypress/schematic": "^2.1.1",
|
||||||
|
"cypress": "^12.9.0"
|
||||||
|
}
|
||||||
|
}
|
31
src-ui/setup-jest.ts
Normal file
31
src-ui/setup-jest.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { jest } from '@jest/globals'
|
||||||
|
|
||||||
|
/* global mocks for jsdom */
|
||||||
|
const mock = () => {
|
||||||
|
let storage: { [key: string]: string } = {}
|
||||||
|
return {
|
||||||
|
getItem: (key: string) => (key in storage ? storage[key] : null),
|
||||||
|
setItem: (key: string, value: string) => (storage[key] = value || ''),
|
||||||
|
removeItem: (key: string) => delete storage[key],
|
||||||
|
clear: () => (storage = {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(window, 'localStorage', { value: mock() })
|
||||||
|
Object.defineProperty(window, 'sessionStorage', { value: mock() })
|
||||||
|
Object.defineProperty(window, 'getComputedStyle', {
|
||||||
|
value: () => ['-webkit-appearance'],
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.defineProperty(document.body.style, 'transform', {
|
||||||
|
value: () => {
|
||||||
|
return {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
HTMLCanvasElement.prototype.getContext = <
|
||||||
|
typeof HTMLCanvasElement.prototype.getContext
|
||||||
|
>jest.fn()
|
197
src-ui/src/app/app-routing.module.ts
Normal file
197
src-ui/src/app/app-routing.module.ts
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
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 { 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 { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
||||||
|
import { DirtyFormGuard } from './guards/dirty-form.guard'
|
||||||
|
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
|
||||||
|
import { TasksComponent } from './components/manage/tasks/tasks.component'
|
||||||
|
import { PermissionsGuard } from './guards/permissions.guard'
|
||||||
|
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
||||||
|
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
|
||||||
|
import {
|
||||||
|
PermissionAction,
|
||||||
|
PermissionType,
|
||||||
|
} from './services/permissions.service'
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: AppFrameComponent,
|
||||||
|
canDeactivate: [DirtyDocGuard],
|
||||||
|
children: [
|
||||||
|
{ path: 'dashboard', component: DashboardComponent },
|
||||||
|
{
|
||||||
|
path: 'documents',
|
||||||
|
component: DocumentListComponent,
|
||||||
|
canDeactivate: [DirtySavedViewGuard],
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.Document,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'view/:id',
|
||||||
|
component: DocumentListComponent,
|
||||||
|
canDeactivate: [DirtySavedViewGuard],
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.SavedView,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'documents/:id',
|
||||||
|
component: DocumentDetailComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.Document,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'documents/:id/:section',
|
||||||
|
component: DocumentDetailComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.Document,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'asn/:id',
|
||||||
|
component: DocumentAsnComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.Document,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tags',
|
||||||
|
component: TagListComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.Tag,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'documenttypes',
|
||||||
|
component: DocumentTypeListComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.DocumentType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'correspondents',
|
||||||
|
component: CorrespondentListComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.Correspondent,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'storagepaths',
|
||||||
|
component: StoragePathListComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.StoragePath,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'logs',
|
||||||
|
component: LogsComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.Admin,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'settings',
|
||||||
|
component: SettingsComponent,
|
||||||
|
canDeactivate: [DirtyFormGuard],
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.UISettings,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'settings/:section',
|
||||||
|
component: SettingsComponent,
|
||||||
|
canDeactivate: [DirtyFormGuard],
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.UISettings,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'settings/:section',
|
||||||
|
component: SettingsComponent,
|
||||||
|
canDeactivate: [DirtyFormGuard],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tasks',
|
||||||
|
component: TasksComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.PaperlessTask,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ path: 'tasks', component: TasksComponent },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{ path: '404', component: NotFoundComponent },
|
||||||
|
{ path: '**', redirectTo: '/404', pathMatch: 'full' },
|
||||||
|
]
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forRoot(routes)],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class AppRoutingModule {}
|
38
src-ui/src/app/app.component.html
Normal file
38
src-ui/src/app/app.component.html
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<app-toasts></app-toasts>
|
||||||
|
|
||||||
|
<ngx-file-drop dropZoneClassName="main-dropzone" contentClassName="main-content" [disabled]="!dragDropEnabled"
|
||||||
|
(onFileDrop)="dropped($event)" (onFileOver)="fileOver()" (onFileLeave)="fileLeave()">
|
||||||
|
<ng-template ngx-file-drop-content-tmp>
|
||||||
|
<div class="global-dropzone-overlay fade" [class.show]="fileIsOver" [class.hide]="hidden">
|
||||||
|
<h2 i18n>Drop files to begin upload</h2>
|
||||||
|
</div>
|
||||||
|
<div [class.inert]="fileIsOver">
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</ngx-file-drop>
|
||||||
|
|
||||||
|
<tour-step-template>
|
||||||
|
<ng-template #tourStep let-step="step">
|
||||||
|
<p class="tour-step-content" [innerHTML]="step?.content"></p>
|
||||||
|
<hr/>
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<span class="badge bg-light text-dark">{{ tourService.steps?.indexOf(step) + 1 }} / {{ tourService.steps?.length }}</span>
|
||||||
|
<div class="tour-step-navigation btn-toolbar" role="toolbar" aria-label="Controls">
|
||||||
|
<div class="btn-group btn-group-sm me-2" role="group" aria-label="Dismiss">
|
||||||
|
<button class="btn btn-outline-danger" (click)="tourService.end()">
|
||||||
|
{{ step?.endBtnTitle }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group btn-group-sm align-self-end" role="group" aria-label="Previous / Next">
|
||||||
|
<button *ngIf="tourService.hasPrev(step)" class="btn btn-outline-primary" (click)="tourService.prev()">
|
||||||
|
« {{ step?.prevBtnTitle }}
|
||||||
|
</button>
|
||||||
|
<button *ngIf="tourService.hasNext(step)" class="btn btn-outline-primary" (click)="tourService.next()">
|
||||||
|
{{ step?.nextBtnTitle }} »
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</tour-step-template>
|
0
src-ui/src/app/app.component.scss
Normal file
0
src-ui/src/app/app.component.scss
Normal file
283
src-ui/src/app/app.component.ts
Normal file
283
src-ui/src/app/app.component.ts
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
import { SettingsService } from './services/settings.service'
|
||||||
|
import { SETTINGS_KEYS } from './data/paperless-uisettings'
|
||||||
|
import { Component, OnDestroy, OnInit, Renderer2 } from '@angular/core'
|
||||||
|
import { Router } from '@angular/router'
|
||||||
|
import { Subscription } from 'rxjs'
|
||||||
|
import { ConsumerStatusService } from './services/consumer-status.service'
|
||||||
|
import { ToastService } from './services/toast.service'
|
||||||
|
import { NgxFileDropEntry } from 'ngx-file-drop'
|
||||||
|
import { UploadDocumentsService } from './services/upload-documents.service'
|
||||||
|
import { TasksService } from './services/tasks.service'
|
||||||
|
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
|
import {
|
||||||
|
PermissionAction,
|
||||||
|
PermissionsService,
|
||||||
|
PermissionType,
|
||||||
|
} from './services/permissions.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
styleUrls: ['./app.component.scss'],
|
||||||
|
})
|
||||||
|
export class AppComponent implements OnInit, OnDestroy {
|
||||||
|
newDocumentSubscription: Subscription
|
||||||
|
successSubscription: Subscription
|
||||||
|
failedSubscription: Subscription
|
||||||
|
|
||||||
|
private fileLeaveTimeoutID: any
|
||||||
|
fileIsOver: boolean = false
|
||||||
|
hidden: boolean = true
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private settings: SettingsService,
|
||||||
|
private consumerStatusService: ConsumerStatusService,
|
||||||
|
private toastService: ToastService,
|
||||||
|
private router: Router,
|
||||||
|
private uploadDocumentsService: UploadDocumentsService,
|
||||||
|
private tasksService: TasksService,
|
||||||
|
public tourService: TourService,
|
||||||
|
private renderer: Renderer2,
|
||||||
|
private permissionsService: PermissionsService
|
||||||
|
) {
|
||||||
|
let anyWindow = window as any
|
||||||
|
anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.js'
|
||||||
|
this.settings.updateAppearanceSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.consumerStatusService.disconnect()
|
||||||
|
if (this.successSubscription) {
|
||||||
|
this.successSubscription.unsubscribe()
|
||||||
|
}
|
||||||
|
if (this.failedSubscription) {
|
||||||
|
this.failedSubscription.unsubscribe()
|
||||||
|
}
|
||||||
|
if (this.newDocumentSubscription) {
|
||||||
|
this.newDocumentSubscription.unsubscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private showNotification(key) {
|
||||||
|
if (
|
||||||
|
this.router.url == '/dashboard' &&
|
||||||
|
this.settings.get(
|
||||||
|
SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return this.settings.get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.consumerStatusService.connect()
|
||||||
|
|
||||||
|
this.successSubscription = this.consumerStatusService
|
||||||
|
.onDocumentConsumptionFinished()
|
||||||
|
.subscribe((status) => {
|
||||||
|
this.tasksService.reload()
|
||||||
|
if (
|
||||||
|
this.showNotification(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS)
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.View,
|
||||||
|
PermissionType.Document
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.toastService.show({
|
||||||
|
title: $localize`Document added`,
|
||||||
|
delay: 10000,
|
||||||
|
content: $localize`Document ${status.filename} was added to paperless.`,
|
||||||
|
actionName: $localize`Open document`,
|
||||||
|
action: () => {
|
||||||
|
this.router.navigate(['documents', status.documentId])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.toastService.show({
|
||||||
|
title: $localize`Document added`,
|
||||||
|
delay: 10000,
|
||||||
|
content: $localize`Document ${status.filename} was added to paperless.`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.failedSubscription = this.consumerStatusService
|
||||||
|
.onDocumentConsumptionFailed()
|
||||||
|
.subscribe((status) => {
|
||||||
|
this.tasksService.reload()
|
||||||
|
if (
|
||||||
|
this.showNotification(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_FAILED)
|
||||||
|
) {
|
||||||
|
this.toastService.showError(
|
||||||
|
$localize`Could not add ${status.filename}\: ${status.message}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.newDocumentSubscription = this.consumerStatusService
|
||||||
|
.onDocumentDetected()
|
||||||
|
.subscribe((status) => {
|
||||||
|
this.tasksService.reload()
|
||||||
|
if (
|
||||||
|
this.showNotification(
|
||||||
|
SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.toastService.show({
|
||||||
|
title: $localize`New document detected`,
|
||||||
|
delay: 5000,
|
||||||
|
content: $localize`Document ${status.filename} is being processed by paperless.`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const prevBtnTitle = $localize`Prev`
|
||||||
|
const nextBtnTitle = $localize`Next`
|
||||||
|
const endBtnTitle = $localize`End`
|
||||||
|
|
||||||
|
this.tourService.initialize([
|
||||||
|
{
|
||||||
|
anchorId: 'tour.dashboard',
|
||||||
|
content: $localize`The dashboard can be used to show saved views, such as an 'Inbox'. Those settings are found under Settings > Saved Views once you have created some.`,
|
||||||
|
route: '/dashboard',
|
||||||
|
enableBackdrop: true,
|
||||||
|
delayAfterNavigation: 500,
|
||||||
|
prevBtnTitle,
|
||||||
|
nextBtnTitle,
|
||||||
|
endBtnTitle,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchorId: 'tour.upload-widget',
|
||||||
|
content: $localize`Drag-and-drop documents here to start uploading or place them in the consume folder. You can also drag-and-drop documents anywhere on all other pages of the web app. Once you do, Paperless-ngx will start training its machine learning algorithms.`,
|
||||||
|
route: '/dashboard',
|
||||||
|
enableBackdrop: true,
|
||||||
|
prevBtnTitle,
|
||||||
|
nextBtnTitle,
|
||||||
|
endBtnTitle,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchorId: 'tour.documents',
|
||||||
|
content: $localize`The documents list shows all of your documents and allows for filtering as well as bulk-editing. There are three different view styles: list, small cards and large cards. A list of documents currently opened for editing is shown in the sidebar.`,
|
||||||
|
route: '/documents?sort=created&reverse=1&page=1',
|
||||||
|
delayAfterNavigation: 500,
|
||||||
|
placement: 'bottom',
|
||||||
|
enableBackdrop: true,
|
||||||
|
disableScrollToAnchor: true,
|
||||||
|
prevBtnTitle,
|
||||||
|
nextBtnTitle,
|
||||||
|
endBtnTitle,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchorId: 'tour.documents-filter-editor',
|
||||||
|
content: $localize`The filtering tools allow you to quickly find documents using various searches, dates, tags, etc.`,
|
||||||
|
route: '/documents?sort=created&reverse=1&page=1',
|
||||||
|
placement: 'bottom',
|
||||||
|
enableBackdrop: true,
|
||||||
|
prevBtnTitle,
|
||||||
|
nextBtnTitle,
|
||||||
|
endBtnTitle,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchorId: 'tour.documents-views',
|
||||||
|
content: $localize`Any combination of filters can be saved as a 'view' which can then be displayed on the dashboard and / or sidebar.`,
|
||||||
|
route: '/documents?sort=created&reverse=1&page=1',
|
||||||
|
enableBackdrop: true,
|
||||||
|
prevBtnTitle,
|
||||||
|
nextBtnTitle,
|
||||||
|
endBtnTitle,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchorId: 'tour.tags',
|
||||||
|
content: $localize`Tags, correspondents, document types and storage paths can all be managed using these pages. They can also be created from the document edit view.`,
|
||||||
|
route: '/tags',
|
||||||
|
enableBackdrop: true,
|
||||||
|
prevBtnTitle,
|
||||||
|
nextBtnTitle,
|
||||||
|
endBtnTitle,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchorId: 'tour.file-tasks',
|
||||||
|
content: $localize`File Tasks shows you documents that have been consumed, are waiting to be, or may have failed during the process.`,
|
||||||
|
route: '/tasks',
|
||||||
|
enableBackdrop: true,
|
||||||
|
prevBtnTitle,
|
||||||
|
nextBtnTitle,
|
||||||
|
endBtnTitle,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchorId: 'tour.settings',
|
||||||
|
content: $localize`Check out the settings for various tweaks to the web app, toggle settings for saved views or setup e-mail checking.`,
|
||||||
|
route: '/settings',
|
||||||
|
enableBackdrop: true,
|
||||||
|
prevBtnTitle,
|
||||||
|
nextBtnTitle,
|
||||||
|
endBtnTitle,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchorId: 'tour.outro',
|
||||||
|
title: $localize`Thank you! 🙏`,
|
||||||
|
content:
|
||||||
|
$localize`There are <em>tons</em> more features and info we didn't cover here, but this should get you started. Check out the documentation or visit the project on GitHub to learn more or to report issues.` +
|
||||||
|
'<br/><br/>' +
|
||||||
|
$localize`Lastly, on behalf of every contributor to this community-supported project, thank you for using Paperless-ngx!`,
|
||||||
|
route: '/dashboard',
|
||||||
|
prevBtnTitle,
|
||||||
|
nextBtnTitle,
|
||||||
|
endBtnTitle,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
this.tourService.start$.subscribe(() => {
|
||||||
|
this.renderer.addClass(document.body, 'tour-active')
|
||||||
|
})
|
||||||
|
|
||||||
|
this.tourService.end$.subscribe(() => {
|
||||||
|
// animation time
|
||||||
|
setTimeout(() => {
|
||||||
|
this.renderer.removeClass(document.body, 'tour-active')
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public get dragDropEnabled(): boolean {
|
||||||
|
return (
|
||||||
|
!this.router.url.includes('dashboard') &&
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.Add,
|
||||||
|
PermissionType.Document
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public fileOver() {
|
||||||
|
// allows transition
|
||||||
|
setTimeout(() => {
|
||||||
|
this.fileIsOver = true
|
||||||
|
}, 1)
|
||||||
|
this.hidden = false
|
||||||
|
// stop fileLeave timeout
|
||||||
|
clearTimeout(this.fileLeaveTimeoutID)
|
||||||
|
}
|
||||||
|
|
||||||
|
public fileLeave(immediate: boolean = false) {
|
||||||
|
const ms = immediate ? 0 : 500
|
||||||
|
|
||||||
|
this.fileLeaveTimeoutID = setTimeout(() => {
|
||||||
|
this.fileIsOver = false
|
||||||
|
// await transition completed
|
||||||
|
setTimeout(() => {
|
||||||
|
this.hidden = true
|
||||||
|
}, 150)
|
||||||
|
}, ms)
|
||||||
|
}
|
||||||
|
|
||||||
|
public dropped(files: NgxFileDropEntry[]) {
|
||||||
|
this.fileLeave(true)
|
||||||
|
this.uploadDocumentsService.uploadFiles(files)
|
||||||
|
this.toastService.showInfo($localize`Initiating upload...`, 3000)
|
||||||
|
}
|
||||||
|
}
|
255
src-ui/src/app/app.module.ts
Normal file
255
src-ui/src/app/app.module.ts
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
import { BrowserModule } from '@angular/platform-browser'
|
||||||
|
import { APP_INITIALIZER, NgModule } from '@angular/core'
|
||||||
|
import { AppRoutingModule } from './app-routing.module'
|
||||||
|
import { AppComponent } from './app.component'
|
||||||
|
import {
|
||||||
|
NgbDateAdapter,
|
||||||
|
NgbDateParserFormatter,
|
||||||
|
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 { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'
|
||||||
|
import { LogsComponent } from './components/manage/logs/logs.component'
|
||||||
|
import { SettingsComponent } from './components/manage/settings/settings.component'
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
import { DatePipe, registerLocaleData } from '@angular/common'
|
||||||
|
import { NotFoundComponent } from './components/not-found/not-found.component'
|
||||||
|
import { ConfirmDialogComponent } from './components/common/confirm-dialog/confirm-dialog.component'
|
||||||
|
import { CorrespondentEditDialogComponent } from './components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
||||||
|
import { TagEditDialogComponent } from './components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
||||||
|
import { DocumentTypeEditDialogComponent } from './components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
||||||
|
import { TagComponent } from './components/common/tag/tag.component'
|
||||||
|
import { ClearableBadgeComponent } from './components/common/clearable-badge/clearable-badge.component'
|
||||||
|
import { PageHeaderComponent } from './components/common/page-header/page-header.component'
|
||||||
|
import { AppFrameComponent } from './components/app-frame/app-frame.component'
|
||||||
|
import { ToastsComponent } from './components/common/toasts/toasts.component'
|
||||||
|
import { FilterEditorComponent } from './components/document-list/filter-editor/filter-editor.component'
|
||||||
|
import { FilterableDropdownComponent } from './components/common/filterable-dropdown/filterable-dropdown.component'
|
||||||
|
import { ToggleableDropdownButtonComponent } from './components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component'
|
||||||
|
import { DateDropdownComponent } from './components/common/date-dropdown/date-dropdown.component'
|
||||||
|
import { DocumentCardLargeComponent } from './components/document-list/document-card-large/document-card-large.component'
|
||||||
|
import { DocumentCardSmallComponent } from './components/document-list/document-card-small/document-card-small.component'
|
||||||
|
import { BulkEditorComponent } from './components/document-list/bulk-editor/bulk-editor.component'
|
||||||
|
import { NgxFileDropModule } from 'ngx-file-drop'
|
||||||
|
import { TextComponent } from './components/common/input/text/text.component'
|
||||||
|
import { SelectComponent } from './components/common/input/select/select.component'
|
||||||
|
import { CheckComponent } from './components/common/input/check/check.component'
|
||||||
|
import { PasswordComponent } from './components/common/input/password/password.component'
|
||||||
|
import { SaveViewConfigDialogComponent } from './components/document-list/save-view-config-dialog/save-view-config-dialog.component'
|
||||||
|
import { TagsComponent } from './components/common/input/tags/tags.component'
|
||||||
|
import { IfPermissionsDirective } from './directives/if-permissions.directive'
|
||||||
|
import { SortableDirective } from './directives/sortable.directive'
|
||||||
|
import { CookieService } from 'ngx-cookie-service'
|
||||||
|
import { CsrfInterceptor } from './interceptors/csrf.interceptor'
|
||||||
|
import { SavedViewWidgetComponent } from './components/dashboard/widgets/saved-view-widget/saved-view-widget.component'
|
||||||
|
import { StatisticsWidgetComponent } from './components/dashboard/widgets/statistics-widget/statistics-widget.component'
|
||||||
|
import { UploadFileWidgetComponent } from './components/dashboard/widgets/upload-file-widget/upload-file-widget.component'
|
||||||
|
import { WidgetFrameComponent } from './components/dashboard/widgets/widget-frame/widget-frame.component'
|
||||||
|
import { PdfViewerModule } from 'ng2-pdf-viewer'
|
||||||
|
import { WelcomeWidgetComponent } from './components/dashboard/widgets/welcome-widget/welcome-widget.component'
|
||||||
|
import { YesNoPipe } from './pipes/yes-no.pipe'
|
||||||
|
import { FileSizePipe } from './pipes/file-size.pipe'
|
||||||
|
import { FilterPipe } from './pipes/filter.pipe'
|
||||||
|
import { DocumentTitlePipe } from './pipes/document-title.pipe'
|
||||||
|
import { MetadataCollapseComponent } from './components/document-detail/metadata-collapse/metadata-collapse.component'
|
||||||
|
import { SelectDialogComponent } from './components/common/select-dialog/select-dialog.component'
|
||||||
|
import { NgSelectModule } from '@ng-select/ng-select'
|
||||||
|
import { NumberComponent } from './components/common/input/number/number.component'
|
||||||
|
import { SafeUrlPipe } from './pipes/safeurl.pipe'
|
||||||
|
import { SafeHtmlPipe } from './pipes/safehtml.pipe'
|
||||||
|
import { CustomDatePipe } from './pipes/custom-date.pipe'
|
||||||
|
import { DateComponent } from './components/common/input/date/date.component'
|
||||||
|
import { ISODateAdapter } from './utils/ngb-iso-date-adapter'
|
||||||
|
import { LocalizedDateParserFormatter } from './utils/ngb-date-parser-formatter'
|
||||||
|
import { ApiVersionInterceptor } from './interceptors/api-version.interceptor'
|
||||||
|
import { ColorSliderModule } from 'ngx-color/slider'
|
||||||
|
import { ColorComponent } from './components/common/input/color/color.component'
|
||||||
|
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
||||||
|
import { DocumentNotesComponent } from './components/document-notes/document-notes.component'
|
||||||
|
import { PermissionsGuard } from './guards/permissions.guard'
|
||||||
|
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
||||||
|
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
|
||||||
|
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
|
||||||
|
import { StoragePathEditDialogComponent } from './components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||||
|
import { SettingsService } from './services/settings.service'
|
||||||
|
import { TasksComponent } from './components/manage/tasks/tasks.component'
|
||||||
|
import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
|
import { UserEditDialogComponent } from './components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
|
||||||
|
import { GroupEditDialogComponent } from './components/common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
|
||||||
|
import { PermissionsSelectComponent } from './components/common/permissions-select/permissions-select.component'
|
||||||
|
import { MailAccountEditDialogComponent } from './components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component'
|
||||||
|
import { MailRuleEditDialogComponent } from './components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component'
|
||||||
|
import { PermissionsUserComponent } from './components/common/input/permissions/permissions-user/permissions-user.component'
|
||||||
|
import { PermissionsGroupComponent } from './components/common/input/permissions/permissions-group/permissions-group.component'
|
||||||
|
import { IfOwnerDirective } from './directives/if-owner.directive'
|
||||||
|
import { IfObjectPermissionsDirective } from './directives/if-object-permissions.directive'
|
||||||
|
|
||||||
|
import localeAr from '@angular/common/locales/ar'
|
||||||
|
import localeBe from '@angular/common/locales/be'
|
||||||
|
import localeCs from '@angular/common/locales/cs'
|
||||||
|
import localeDa from '@angular/common/locales/da'
|
||||||
|
import localeDe from '@angular/common/locales/de'
|
||||||
|
import localeEnGb from '@angular/common/locales/en-GB'
|
||||||
|
import localeEs from '@angular/common/locales/es'
|
||||||
|
import localeFr from '@angular/common/locales/fr'
|
||||||
|
import localeIt from '@angular/common/locales/it'
|
||||||
|
import localeLb from '@angular/common/locales/lb'
|
||||||
|
import localeNl from '@angular/common/locales/nl'
|
||||||
|
import localePl from '@angular/common/locales/pl'
|
||||||
|
import localePt from '@angular/common/locales/pt'
|
||||||
|
import localeRo from '@angular/common/locales/ro'
|
||||||
|
import localeRu from '@angular/common/locales/ru'
|
||||||
|
import localeSl from '@angular/common/locales/sl'
|
||||||
|
import localeSr from '@angular/common/locales/sr'
|
||||||
|
import localeSv from '@angular/common/locales/sv'
|
||||||
|
import localeTr from '@angular/common/locales/tr'
|
||||||
|
import localeZh from '@angular/common/locales/zh'
|
||||||
|
import { PermissionsDialogComponent } from './components/common/permissions-dialog/permissions-dialog.component'
|
||||||
|
import { PermissionsFormComponent } from './components/common/input/permissions/permissions-form/permissions-form.component'
|
||||||
|
|
||||||
|
registerLocaleData(localeAr)
|
||||||
|
registerLocaleData(localeBe)
|
||||||
|
registerLocaleData(localeCs)
|
||||||
|
registerLocaleData(localeDa)
|
||||||
|
registerLocaleData(localeDe)
|
||||||
|
registerLocaleData(localeEnGb)
|
||||||
|
registerLocaleData(localeEs)
|
||||||
|
registerLocaleData(localeFr)
|
||||||
|
registerLocaleData(localeIt)
|
||||||
|
registerLocaleData(localeLb)
|
||||||
|
registerLocaleData(localeNl)
|
||||||
|
registerLocaleData(localePl)
|
||||||
|
registerLocaleData(localePt, 'pt-BR')
|
||||||
|
registerLocaleData(localePt, 'pt-PT')
|
||||||
|
registerLocaleData(localeRo)
|
||||||
|
registerLocaleData(localeRu)
|
||||||
|
registerLocaleData(localeSl)
|
||||||
|
registerLocaleData(localeSr)
|
||||||
|
registerLocaleData(localeSv)
|
||||||
|
registerLocaleData(localeTr)
|
||||||
|
registerLocaleData(localeZh)
|
||||||
|
|
||||||
|
function initializeApp(settings: SettingsService) {
|
||||||
|
return () => {
|
||||||
|
return settings.initializeSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AppComponent,
|
||||||
|
DocumentListComponent,
|
||||||
|
DocumentDetailComponent,
|
||||||
|
DashboardComponent,
|
||||||
|
TagListComponent,
|
||||||
|
DocumentTypeListComponent,
|
||||||
|
CorrespondentListComponent,
|
||||||
|
StoragePathListComponent,
|
||||||
|
LogsComponent,
|
||||||
|
SettingsComponent,
|
||||||
|
NotFoundComponent,
|
||||||
|
CorrespondentEditDialogComponent,
|
||||||
|
ConfirmDialogComponent,
|
||||||
|
TagEditDialogComponent,
|
||||||
|
DocumentTypeEditDialogComponent,
|
||||||
|
StoragePathEditDialogComponent,
|
||||||
|
TagComponent,
|
||||||
|
ClearableBadgeComponent,
|
||||||
|
PageHeaderComponent,
|
||||||
|
AppFrameComponent,
|
||||||
|
ToastsComponent,
|
||||||
|
FilterEditorComponent,
|
||||||
|
FilterableDropdownComponent,
|
||||||
|
ToggleableDropdownButtonComponent,
|
||||||
|
DateDropdownComponent,
|
||||||
|
DocumentCardLargeComponent,
|
||||||
|
DocumentCardSmallComponent,
|
||||||
|
BulkEditorComponent,
|
||||||
|
TextComponent,
|
||||||
|
SelectComponent,
|
||||||
|
CheckComponent,
|
||||||
|
PasswordComponent,
|
||||||
|
SaveViewConfigDialogComponent,
|
||||||
|
TagsComponent,
|
||||||
|
IfPermissionsDirective,
|
||||||
|
SortableDirective,
|
||||||
|
SavedViewWidgetComponent,
|
||||||
|
StatisticsWidgetComponent,
|
||||||
|
UploadFileWidgetComponent,
|
||||||
|
WidgetFrameComponent,
|
||||||
|
WelcomeWidgetComponent,
|
||||||
|
YesNoPipe,
|
||||||
|
FileSizePipe,
|
||||||
|
FilterPipe,
|
||||||
|
DocumentTitlePipe,
|
||||||
|
MetadataCollapseComponent,
|
||||||
|
SelectDialogComponent,
|
||||||
|
NumberComponent,
|
||||||
|
SafeUrlPipe,
|
||||||
|
SafeHtmlPipe,
|
||||||
|
CustomDatePipe,
|
||||||
|
DateComponent,
|
||||||
|
ColorComponent,
|
||||||
|
DocumentAsnComponent,
|
||||||
|
DocumentNotesComponent,
|
||||||
|
TasksComponent,
|
||||||
|
UserEditDialogComponent,
|
||||||
|
GroupEditDialogComponent,
|
||||||
|
PermissionsSelectComponent,
|
||||||
|
MailAccountEditDialogComponent,
|
||||||
|
MailRuleEditDialogComponent,
|
||||||
|
PermissionsUserComponent,
|
||||||
|
PermissionsGroupComponent,
|
||||||
|
IfOwnerDirective,
|
||||||
|
IfObjectPermissionsDirective,
|
||||||
|
PermissionsDialogComponent,
|
||||||
|
PermissionsFormComponent,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
AppRoutingModule,
|
||||||
|
NgbModule,
|
||||||
|
HttpClientModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
NgxFileDropModule,
|
||||||
|
PdfViewerModule,
|
||||||
|
NgSelectModule,
|
||||||
|
ColorSliderModule,
|
||||||
|
TourNgBootstrapModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: APP_INITIALIZER,
|
||||||
|
useFactory: initializeApp,
|
||||||
|
deps: [SettingsService],
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
DatePipe,
|
||||||
|
CookieService,
|
||||||
|
{
|
||||||
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
useClass: CsrfInterceptor,
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
useClass: ApiVersionInterceptor,
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
FilterPipe,
|
||||||
|
DocumentTitlePipe,
|
||||||
|
{ provide: NgbDateAdapter, useClass: ISODateAdapter },
|
||||||
|
{ provide: NgbDateParserFormatter, useClass: LocalizedDateParserFormatter },
|
||||||
|
PermissionsGuard,
|
||||||
|
DirtyDocGuard,
|
||||||
|
DirtySavedViewGuard,
|
||||||
|
],
|
||||||
|
bootstrap: [AppComponent],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
257
src-ui/src/app/components/app-frame/app-frame.component.html
Normal file
257
src-ui/src/app/components/app-frame/app-frame.component.html
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
<nav class="navbar navbar-dark sticky-top bg-primary flex-md-nowrap p-0 shadow">
|
||||||
|
<button class="navbar-toggler d-md-none collapsed border-0" type="button" data-toggle="collapse"
|
||||||
|
data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation"
|
||||||
|
(click)="isMenuCollapsed = !isMenuCollapsed">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<a class="navbar-brand col-auto col-md-3 col-lg-2 me-0 px-3 py-3 order-sm-0" [ngClass]="slimSidebarEnabled ? 'slim' : 'col-auto col-md-3 col-lg-2'" routerLink="/dashboard" tourAnchor="tour.intro">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.43 238.91" width="1em" class="me-2" fill="currentColor">
|
||||||
|
<path d="M194.7,0C164.22,70.94,17.64,79.74,64.55,194.06c.58,1.47-10.85,17-18.47,29.9-1.76-6.45-3.81-13.48-3.52-14.07,38.11-45.14-27.26-70.65-30.78-107.58C-4.64,131.62-10.5,182.92,39,212.53c.3,0,2.64,11.14,3.81,16.71a58.55,58.55,0,0,0-2.93,6.45c-1.17,2.93,7.62,2.64,7.62,3.22.88-.29,21.7-36.93,22.28-37.23C187.67,174.72,208.48,68.6,194.7,0ZM134.61,74.75C79.5,124,70.12,160.64,71.88,178.53,53.41,134.85,107.64,86.77,134.61,74.75ZM28.2,145.11c10.55,9.67,28.14,39.28,13.19,56.57C44.91,193.77,46.08,175.89,28.2,145.11Z" transform="translate(0 0)"/>
|
||||||
|
</svg>
|
||||||
|
<span class="ms-2" [class.visually-hidden]="slimSidebarEnabled" i18n="app title">Paperless-ngx</span>
|
||||||
|
</a>
|
||||||
|
<div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 ps-md-4 me-sm-auto order-3 order-sm-1" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||||
|
<form (ngSubmit)="search()" class="form-inline flex-grow-1">
|
||||||
|
<svg width="1em" height="1em" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#search"/>
|
||||||
|
</svg>
|
||||||
|
<input class="form-control form-control-sm" type="text" placeholder="Search documents" aria-label="Search"
|
||||||
|
[formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (keyup)="searchFieldKeyup($event)" (selectItem)="itemSelected($event)" i18n-placeholder>
|
||||||
|
<button type="button" *ngIf="!searchFieldEmpty" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0" (click)="resetSearchField()">
|
||||||
|
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x me-1" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<ul ngbNav class="order-sm-3">
|
||||||
|
<li ngbDropdown class="nav-item dropdown">
|
||||||
|
<button class="btn border-0" id="userDropdown" ngbDropdownToggle>
|
||||||
|
<span class="small me-2 d-none d-sm-inline">
|
||||||
|
{{this.settingsService.displayName}}
|
||||||
|
</span>
|
||||||
|
<svg width="1.3em" height="1.3em" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#person-circle"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div ngbDropdownMenu class="dropdown-menu-end shadow me-2" aria-labelledby="userDropdown">
|
||||||
|
<div class="d-sm-none">
|
||||||
|
<p class="small mb-0 px-3 text-muted" i18n>Logged in as {{this.settingsService.displayName}}</p>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
</div>
|
||||||
|
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }">
|
||||||
|
<svg class="sidebaricon me-2" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
|
||||||
|
</svg><ng-container i18n>Settings</ng-container>
|
||||||
|
</a>
|
||||||
|
<a ngbDropdownItem class="nav-link" href="accounts/logout/">
|
||||||
|
<svg class="sidebaricon me-2" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#door-open"/>
|
||||||
|
</svg><ng-container i18n>Logout</ng-container>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<nav id="sidebarMenu" class="d-md-block bg-light sidebar collapse" [ngClass]="slimSidebarEnabled ? 'slim' : 'col-md-3 col-lg-2'" [class.animating]="slimSidebarAnimating" [ngbCollapse]="isMenuCollapsed">
|
||||||
|
<button class="btn btn-sm btn-dark sidebar-slim-toggler" (click)="toggleSlimSidebar()">
|
||||||
|
<svg class="sidebaricon-sm" fill="currentColor">
|
||||||
|
<use *ngIf="slimSidebarEnabled" xlink:href="assets/bootstrap-icons.svg#chevron-double-right"/>
|
||||||
|
<use *ngIf="!slimSidebarEnabled" xlink:href="assets/bootstrap-icons.svg#chevron-double-left"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="sidebar-sticky pt-3 d-flex flex-column justify-space-around">
|
||||||
|
<ul class="nav flex-column">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" routerLink="dashboard" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Dashboard" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#house"/>
|
||||||
|
</svg><span> <ng-container i18n>Dashboard</ng-container></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||||
|
<a class="nav-link" routerLink="documents" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Documents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#files"/>
|
||||||
|
</svg><span> <ng-container i18n>Documents</ng-container></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
|
||||||
|
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted" *ngIf='savedViewService.loading || savedViewService.sidebarViews.length > 0'>
|
||||||
|
<span i18n>Saved views</span>
|
||||||
|
<div *ngIf="savedViewService.loading" class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div>
|
||||||
|
</h6>
|
||||||
|
<ul class="nav flex-column mb-2">
|
||||||
|
<li class="nav-item w-100" *ngFor="let view of savedViewService.sidebarViews">
|
||||||
|
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="view/{{view.id}}" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name" [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#funnel"/>
|
||||||
|
</svg><span> {{view.name}}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||||
|
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted" *ngIf='openDocuments.length > 0'>
|
||||||
|
<span i18n>Open documents</span>
|
||||||
|
</h6>
|
||||||
|
<ul class="nav flex-column mb-2">
|
||||||
|
<li class="nav-item w-100" *ngFor='let d of openDocuments'>
|
||||||
|
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="documents/{{d.id}}" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="d.title | documentTitle" [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
|
||||||
|
</svg><span> {{d.title | documentTitle}}</span>
|
||||||
|
<span class="close" (click)="closeDocument(d); $event.preventDefault()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-x" viewBox="0 0 16 16">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item w-100" *ngIf="openDocuments.length >= 1">
|
||||||
|
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" [routerLink]="[]" (click)="closeAll()" ngbPopover="Close all" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||||
|
</svg><span> <ng-container i18n>Close all</ng-container></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted">
|
||||||
|
<span i18n>Manage</span>
|
||||||
|
</h6>
|
||||||
|
<ul class="nav flex-column mb-2">
|
||||||
|
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
|
||||||
|
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Correspondents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#person"/>
|
||||||
|
</svg><span> <ng-container i18n>Correspondents</ng-container></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }" tourAnchor="tour.tags">
|
||||||
|
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Tags" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#tags"/>
|
||||||
|
</svg><span> <ng-container i18n>Tags</ng-container></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
|
||||||
|
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Document types" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#hash"/>
|
||||||
|
</svg><span> <ng-container i18n>Document types</ng-container></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
|
||||||
|
<a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Storage paths" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#folder"/>
|
||||||
|
</svg><span> <ng-container i18n>Storage paths</ng-container></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.PaperlessTask }" tourAnchor="tour.file-tasks">
|
||||||
|
<a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()" ngbPopover="File Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
|
<span *ngIf="tasksService.failedFileTasks.length > 0 && slimSidebarEnabled" class="badge bg-danger position-absolute top-0 end-0">{{tasksService.failedFileTasks.length}}</span>
|
||||||
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#list-task"/>
|
||||||
|
</svg><span> <ng-container i18n>File Tasks<span *ngIf="tasksService.failedFileTasks.length > 0"><span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span></span></ng-container></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }">
|
||||||
|
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Logs" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#text-left"/>
|
||||||
|
</svg><span> <ng-container i18n>Logs</ng-container></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }" tourAnchor="tour.settings">
|
||||||
|
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Settings" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
|
||||||
|
</svg><span> <ng-container i18n>Settings</ng-container></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h6 class="sidebar-heading px-3 mt-auto pt-4 mb-1 text-muted">
|
||||||
|
<span i18n>Info</span>
|
||||||
|
</h6>
|
||||||
|
<ul class="nav flex-column mb-2">
|
||||||
|
<li class="nav-item" tourAnchor="tour.outro">
|
||||||
|
<a class="nav-link" target="_blank" rel="noopener noreferrer" href="https://docs.paperless-ngx.com" ngbPopover="Documentation" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#question-circle"/>
|
||||||
|
</svg><span> <ng-container i18n>Documentation</ng-container></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<div class="d-flex w-100 flex-wrap">
|
||||||
|
<a class="nav-link pe-2 pb-1" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx" ngbPopover="GitHub" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="sidebaricon" viewBox="0 0 16 16">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#github" />
|
||||||
|
</svg><span> <ng-container i18n>GitHub</ng-container></span>
|
||||||
|
</a>
|
||||||
|
<a class="nav-link-additional small text-muted ms-3" [class.visually-hidden]="slimSidebarEnabled" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx/discussions/categories/feature-requests" title="Suggest an idea" i18n-title>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1.1em" height="1.1em" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#lightbulb" />
|
||||||
|
</svg>
|
||||||
|
<ng-container i18n>Suggest an idea</ng-container>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item mt-2" [class.visually-hidden]="slimSidebarEnabled">
|
||||||
|
<div class="px-3 py-2 text-muted small d-flex align-items-center flex-wrap">
|
||||||
|
<div class="me-3">{{ versionString }}</div>
|
||||||
|
<div *ngIf="!settingsService.updateCheckingIsSet || appRemoteVersion" class="version-check">
|
||||||
|
<ng-template #updateAvailablePopContent>
|
||||||
|
<span class="small">Paperless-ngx {{ appRemoteVersion.version }} <ng-container i18n>is available.</ng-container><br/><ng-container i18n>Click to view.</ng-container></span>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template #updateCheckingNotEnabledPopContent>
|
||||||
|
<p class="small mb-2">
|
||||||
|
<ng-container i18n>Paperless-ngx can automatically check for updates</ng-container>
|
||||||
|
</p>
|
||||||
|
<div class="btn-group btn-group-xs flex-fill w-100">
|
||||||
|
<button class="btn btn-outline-primary" (click)="setUpdateChecking(true)">Enable</button>
|
||||||
|
<button class="btn btn-outline-secondary" (click)="setUpdateChecking(false)">Disable</button>
|
||||||
|
</div>
|
||||||
|
<p class="small mb-0 mt-2">
|
||||||
|
<a class="small text-decoration-none fst-italic" routerLink="/settings" fragment="update-checking" i18n>
|
||||||
|
How does this work?
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</ng-template>
|
||||||
|
<ng-container *ngIf="settingsService.updateCheckingIsSet; else updateCheckNotSet">
|
||||||
|
<a *ngIf="appRemoteVersion.update_available" class="small text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx/releases"
|
||||||
|
[ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave" container="body">
|
||||||
|
<svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#info-circle" />
|
||||||
|
</svg>
|
||||||
|
<ng-container *ngIf="appRemoteVersion?.update_available" i18n>Update available</ng-container>
|
||||||
|
</a>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #updateCheckNotSet>
|
||||||
|
<a class="small text-decoration-none" routerLink="/settings" fragment="update-checking"
|
||||||
|
[ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter" container="body">
|
||||||
|
<svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#info-circle" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main role="main" class="ms-sm-auto px-md-4" [ngClass]="slimSidebarEnabled ? 'col-slim' : 'col-md-9 col-lg-10'">
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
311
src-ui/src/app/components/app-frame/app-frame.component.scss
Normal file
311
src-ui/src/app/components/app-frame/app-frame.component.scss
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
@import "node_modules/bootstrap/scss/functions";
|
||||||
|
@import "node_modules/bootstrap/scss/variables";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Sidebar
|
||||||
|
*/
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 995; /* Behind the navbar */
|
||||||
|
padding: 50px 0 0; /* Height of navbar */
|
||||||
|
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||||
|
|
||||||
|
.sidebar-heading .spinner-border {
|
||||||
|
width: 0.8em;
|
||||||
|
height: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
// These come from the col-md-3 col-lg-2 classes for regular sidebar, needed for animation
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
max-width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
max-width: 16.66666667%;
|
||||||
|
}
|
||||||
|
|
||||||
|
transition: all .2s ease;
|
||||||
|
}
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.sidebar {
|
||||||
|
top: 3.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
transition: all .2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-slim-toggler {
|
||||||
|
display: none; // hide on mobile
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar li.nav-item span,
|
||||||
|
.sidebar .sidebar-heading span {
|
||||||
|
transition: all .1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(min-width: 768px) {
|
||||||
|
.sidebar.slim {
|
||||||
|
max-width: 50px;
|
||||||
|
|
||||||
|
li.nav-item span.badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.slim:not(.animating) {
|
||||||
|
li.nav-item span,
|
||||||
|
.sidebar-heading span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.animating {
|
||||||
|
li.nav-item span,
|
||||||
|
.sidebar-heading span {
|
||||||
|
display: unset;
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar:not(.slim):not(.animating) {
|
||||||
|
li.nav-item span,
|
||||||
|
.sidebar-heading span {
|
||||||
|
position: unset;
|
||||||
|
opacity: 1;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.slim,
|
||||||
|
.sidebar.animating {
|
||||||
|
.text-truncate {
|
||||||
|
text-overflow: unset !important;
|
||||||
|
word-wrap: break-word !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.slim {
|
||||||
|
li.nav-item span.badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-slim {
|
||||||
|
padding-left: calc(50px + $grid-gutter-width) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-slim-toggler {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
right: -12px;
|
||||||
|
top: 60px;
|
||||||
|
z-index: 996;
|
||||||
|
--bs-btn-padding-x: 0.35rem;
|
||||||
|
--bs-btn-padding-y: 0.125rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .popover-slim .popover-body {
|
||||||
|
--bs-popover-body-padding-x: .5rem;
|
||||||
|
--bs-popover-body-padding-y: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-sticky {
|
||||||
|
position: relative;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
|
||||||
|
min-height: min-content;
|
||||||
|
}
|
||||||
|
@supports ((position: -webkit-sticky) or (position: sticky)) {
|
||||||
|
.sidebar-sticky {
|
||||||
|
position: -webkit-sticky;
|
||||||
|
position: sticky;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-link {
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&:hover, &.active, &:focus {
|
||||||
|
color: var(--bs-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
background-color: var(--bs-body-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebaricon {
|
||||||
|
margin-right: 4px;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-heading {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover .close {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
display: none;
|
||||||
|
position: absolute !important;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 1;
|
||||||
|
top: 0;
|
||||||
|
padding: .25rem .3rem 0;
|
||||||
|
right: .4rem;
|
||||||
|
width: 1.8rem;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover svg {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-additional {
|
||||||
|
margin-top: 0.1rem;
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Navbar
|
||||||
|
*/
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.navbar-brand.slim {
|
||||||
|
max-width: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown.show .dropdown-toggle,
|
||||||
|
.dropdown-toggle:hover {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-toggle::after {
|
||||||
|
margin-left: 0.4em;
|
||||||
|
vertical-align: 0.155em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .dropdown-menu {
|
||||||
|
font-size: 0.875rem; // body size
|
||||||
|
|
||||||
|
a svg {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .search-form-container {
|
||||||
|
max-width: 550px;
|
||||||
|
|
||||||
|
form {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
> svg {
|
||||||
|
position: absolute;
|
||||||
|
left: 0.6rem;
|
||||||
|
top: 0.5rem;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
form > svg {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
background-color: rgba(0, 0, 0, 0.15);
|
||||||
|
padding-left: 1.8rem;
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
transition: all .3s ease, padding-left 0s ease, background-color 0s ease; // Safari requires all
|
||||||
|
max-width: 600px;
|
||||||
|
min-width: 300px; // 1/2 max
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
color: var(--bs-light);
|
||||||
|
flex-grow: 1;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-check {
|
||||||
|
animation: pulse 2s ease-in-out 0s 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
}
|
231
src-ui/src/app/components/app-frame/app-frame.component.ts
Normal file
231
src-ui/src/app/components/app-frame/app-frame.component.ts
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
import { Component, HostListener, OnInit } from '@angular/core'
|
||||||
|
import { FormControl } from '@angular/forms'
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
|
import { from, Observable } from 'rxjs'
|
||||||
|
import {
|
||||||
|
debounceTime,
|
||||||
|
distinctUntilChanged,
|
||||||
|
map,
|
||||||
|
switchMap,
|
||||||
|
first,
|
||||||
|
} from 'rxjs/operators'
|
||||||
|
import { PaperlessDocument } from 'src/app/data/paperless-document'
|
||||||
|
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||||
|
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||||
|
import { SearchService } from 'src/app/services/rest/search.service'
|
||||||
|
import { environment } from 'src/environments/environment'
|
||||||
|
import { DocumentDetailComponent } from '../document-detail/document-detail.component'
|
||||||
|
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||||
|
import { FILTER_FULLTEXT_QUERY } from 'src/app/data/filter-rule-type'
|
||||||
|
import {
|
||||||
|
RemoteVersionService,
|
||||||
|
AppRemoteVersion,
|
||||||
|
} from 'src/app/services/rest/remote-version.service'
|
||||||
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
import { TasksService } from 'src/app/services/tasks.service'
|
||||||
|
import { ComponentCanDeactivate } from 'src/app/guards/dirty-doc.guard'
|
||||||
|
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||||
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-app-frame',
|
||||||
|
templateUrl: './app-frame.component.html',
|
||||||
|
styleUrls: ['./app-frame.component.scss'],
|
||||||
|
})
|
||||||
|
export class AppFrameComponent
|
||||||
|
extends ComponentWithPermissions
|
||||||
|
implements OnInit, ComponentCanDeactivate
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
public router: Router,
|
||||||
|
private activatedRoute: ActivatedRoute,
|
||||||
|
private openDocumentsService: OpenDocumentsService,
|
||||||
|
private searchService: SearchService,
|
||||||
|
public savedViewService: SavedViewService,
|
||||||
|
private remoteVersionService: RemoteVersionService,
|
||||||
|
private list: DocumentListViewService,
|
||||||
|
public settingsService: SettingsService,
|
||||||
|
public tasksService: TasksService,
|
||||||
|
private readonly toastService: ToastService
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
if (this.settingsService.get(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED)) {
|
||||||
|
this.checkForUpdates()
|
||||||
|
}
|
||||||
|
this.tasksService.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
versionString = `${environment.appTitle} ${environment.version}`
|
||||||
|
appRemoteVersion
|
||||||
|
|
||||||
|
isMenuCollapsed: boolean = true
|
||||||
|
|
||||||
|
slimSidebarAnimating: boolean = false
|
||||||
|
|
||||||
|
toggleSlimSidebar(): void {
|
||||||
|
this.slimSidebarAnimating = true
|
||||||
|
this.slimSidebarEnabled = !this.slimSidebarEnabled
|
||||||
|
setTimeout(() => {
|
||||||
|
this.slimSidebarAnimating = false
|
||||||
|
}, 200) // slightly longer than css animation for slim sidebar
|
||||||
|
}
|
||||||
|
|
||||||
|
get slimSidebarEnabled(): boolean {
|
||||||
|
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
|
||||||
|
}
|
||||||
|
|
||||||
|
set slimSidebarEnabled(enabled: boolean) {
|
||||||
|
this.settingsService.set(SETTINGS_KEYS.SLIM_SIDEBAR, enabled)
|
||||||
|
this.settingsService
|
||||||
|
.storeSettings()
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe({
|
||||||
|
error: (error) => {
|
||||||
|
this.toastService.showError(
|
||||||
|
$localize`An error occurred while saving settings.`
|
||||||
|
)
|
||||||
|
console.log(error)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
closeMenu() {
|
||||||
|
this.isMenuCollapsed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
get openDocuments(): PaperlessDocument[] {
|
||||||
|
return this.openDocumentsService.getOpenDocuments()
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('window:beforeunload')
|
||||||
|
canDeactivate(): Observable<boolean> | boolean {
|
||||||
|
return !this.openDocumentsService.hasDirty()
|
||||||
|
}
|
||||||
|
|
||||||
|
searchField = new FormControl('')
|
||||||
|
|
||||||
|
get searchFieldEmpty(): boolean {
|
||||||
|
return this.searchField.value.trim().length == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
resetSearchField() {
|
||||||
|
this.searchField.reset('')
|
||||||
|
}
|
||||||
|
|
||||||
|
searchFieldKeyup(event: KeyboardEvent) {
|
||||||
|
if (event.key == 'Escape') {
|
||||||
|
this.resetSearchField()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
searchAutoComplete = (text$: Observable<string>) =>
|
||||||
|
text$.pipe(
|
||||||
|
debounceTime(200),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map((term) => {
|
||||||
|
if (term.lastIndexOf(' ') != -1) {
|
||||||
|
return term.substring(term.lastIndexOf(' ') + 1)
|
||||||
|
} else {
|
||||||
|
return term
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
switchMap((term) =>
|
||||||
|
term.length < 2 ? from([[]]) : this.searchService.autocomplete(term)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
itemSelected(event) {
|
||||||
|
event.preventDefault()
|
||||||
|
let currentSearch: string = this.searchField.value
|
||||||
|
let lastSpaceIndex = currentSearch.lastIndexOf(' ')
|
||||||
|
if (lastSpaceIndex != -1) {
|
||||||
|
currentSearch = currentSearch.substring(0, lastSpaceIndex + 1)
|
||||||
|
currentSearch += event.item + ' '
|
||||||
|
} else {
|
||||||
|
currentSearch = event.item + ' '
|
||||||
|
}
|
||||||
|
this.searchField.patchValue(currentSearch)
|
||||||
|
}
|
||||||
|
|
||||||
|
search() {
|
||||||
|
this.closeMenu()
|
||||||
|
this.list.quickFilter([
|
||||||
|
{
|
||||||
|
rule_type: FILTER_FULLTEXT_QUERY,
|
||||||
|
value: (this.searchField.value as string).trim(),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDocument(d: PaperlessDocument) {
|
||||||
|
this.openDocumentsService
|
||||||
|
.closeDocument(d)
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe((confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.closeMenu()
|
||||||
|
let route = this.activatedRoute.snapshot
|
||||||
|
while (route.firstChild) {
|
||||||
|
route = route.firstChild
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
route.component == DocumentDetailComponent &&
|
||||||
|
route.params['id'] == d.id
|
||||||
|
) {
|
||||||
|
this.router.navigate([''])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
closeAll() {
|
||||||
|
// user may need to confirm losing unsaved changes
|
||||||
|
this.openDocumentsService
|
||||||
|
.closeAll()
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe((confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.closeMenu()
|
||||||
|
|
||||||
|
// TODO: is there a better way to do this?
|
||||||
|
let route = this.activatedRoute
|
||||||
|
while (route.firstChild) {
|
||||||
|
route = route.firstChild
|
||||||
|
}
|
||||||
|
if (route.component === DocumentDetailComponent) {
|
||||||
|
this.router.navigate([''])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkForUpdates() {
|
||||||
|
this.remoteVersionService
|
||||||
|
.checkForUpdates()
|
||||||
|
.subscribe((appRemoteVersion: AppRemoteVersion) => {
|
||||||
|
this.appRemoteVersion = appRemoteVersion
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setUpdateChecking(enable: boolean) {
|
||||||
|
this.settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, enable)
|
||||||
|
this.settingsService
|
||||||
|
.storeSettings()
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe({
|
||||||
|
error: (error) => {
|
||||||
|
this.toastService.showError(
|
||||||
|
$localize`An error occurred while saving update checking settings.`
|
||||||
|
)
|
||||||
|
console.log(error)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (enable) {
|
||||||
|
this.checkForUpdates()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
<button *ngIf="active" class="position-absolute top-0 start-100 translate-middle badge bg-secondary border border-light rounded-pill p-1" title="Clear" i18n-title (click)="onClick($event)">
|
||||||
|
<svg *ngIf="!isNumbered && selected" width="1em" height="1em" class="check m-0 p-0 opacity-75" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#check-lg"/>
|
||||||
|
</svg>
|
||||||
|
<div *ngIf="isNumbered" class="number">{{number}}<span class="visually-hidden">selected</span></div>
|
||||||
|
<svg width=".9em" height="1em" class="x m-0 p-0 opacity-75" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#x-lg"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
@ -0,0 +1,28 @@
|
|||||||
|
.badge {
|
||||||
|
min-width: 20px;
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number {
|
||||||
|
min-width: 1em;
|
||||||
|
min-height: 1em;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
.check,
|
||||||
|
.number {
|
||||||
|
opacity: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x {
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
left: calc(50% - 4px);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
import { Component, Input, Output, EventEmitter } from '@angular/core'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-clearable-badge',
|
||||||
|
templateUrl: './clearable-badge.component.html',
|
||||||
|
styleUrls: ['./clearable-badge.component.scss'],
|
||||||
|
})
|
||||||
|
export class ClearableBadgeComponent {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
number: number
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
selected: boolean
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
cleared: EventEmitter<boolean> = new EventEmitter()
|
||||||
|
|
||||||
|
get active(): boolean {
|
||||||
|
return this.selected || this.number > -1
|
||||||
|
}
|
||||||
|
|
||||||
|
get isNumbered(): boolean {
|
||||||
|
return this.number > -1
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick(event: PointerEvent) {
|
||||||
|
this.cleared.emit(true)
|
||||||
|
event.stopImmediatePropagation()
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
|
||||||
|
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p *ngIf="messageBold"><b>{{messageBold}}</b></p>
|
||||||
|
<p class="mb-0" *ngIf="message" [innerHTML]="message | safeHtml"></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" [disabled]="!buttonsEnabled" i18n>
|
||||||
|
<span class="d-inline-block" style="padding-bottom: 1px;" >Cancel</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled">
|
||||||
|
{{btnCaption}}
|
||||||
|
<ngb-progressbar *ngIf="!confirmButtonEnabled" style="height: 1px;" type="dark" [max]="secondsTotal" [value]="seconds"></ngb-progressbar>
|
||||||
|
<span class="visually-hidden">{{ seconds | number: '1.0-0' }} seconds</span>
|
||||||
|
</button>
|
||||||
|
<button *ngIf="alternativeBtnCaption" type="button" class="btn" [class]="alternativeBtnClass" (click)="alternative()" [disabled]="!alternativeButtonEnabled || !buttonsEnabled">
|
||||||
|
{{alternativeBtnCaption}}
|
||||||
|
</button>
|
||||||
|
</div>
|
@ -0,0 +1,88 @@
|
|||||||
|
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { interval, Subject, take } from 'rxjs'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-confirm-dialog',
|
||||||
|
templateUrl: './confirm-dialog.component.html',
|
||||||
|
styleUrls: ['./confirm-dialog.component.scss'],
|
||||||
|
})
|
||||||
|
export class ConfirmDialogComponent {
|
||||||
|
constructor(public activeModal: NgbActiveModal) {}
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
public confirmClicked = new EventEmitter()
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
public alternativeClicked = new EventEmitter()
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
title = $localize`Confirmation`
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
messageBold
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
message
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
btnClass = 'btn-primary'
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
btnCaption = $localize`Confirm`
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
alternativeBtnClass = 'btn-secondary'
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
alternativeBtnCaption
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
buttonsEnabled = true
|
||||||
|
|
||||||
|
confirmButtonEnabled = true
|
||||||
|
alternativeButtonEnabled = true
|
||||||
|
seconds = 0
|
||||||
|
secondsTotal = 0
|
||||||
|
|
||||||
|
confirmSubject: Subject<boolean>
|
||||||
|
alternativeSubject: Subject<boolean>
|
||||||
|
|
||||||
|
delayConfirm(seconds: number) {
|
||||||
|
const refreshInterval = 0.15 // s
|
||||||
|
|
||||||
|
this.secondsTotal = seconds
|
||||||
|
this.seconds = seconds
|
||||||
|
|
||||||
|
interval(refreshInterval * 1000)
|
||||||
|
.pipe(
|
||||||
|
take(this.secondsTotal / refreshInterval + 2) // need 2 more for animation to complete after 0
|
||||||
|
)
|
||||||
|
.subscribe((count) => {
|
||||||
|
this.seconds = Math.max(
|
||||||
|
0,
|
||||||
|
this.secondsTotal - refreshInterval * (count + 1)
|
||||||
|
)
|
||||||
|
this.confirmButtonEnabled =
|
||||||
|
this.secondsTotal - refreshInterval * count < 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.confirmSubject?.next(false)
|
||||||
|
this.confirmSubject?.complete()
|
||||||
|
this.activeModal.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm() {
|
||||||
|
this.confirmClicked.emit()
|
||||||
|
this.confirmSubject?.next(true)
|
||||||
|
this.confirmSubject?.complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
alternative() {
|
||||||
|
this.alternativeClicked.emit()
|
||||||
|
this.alternativeSubject?.next(true)
|
||||||
|
this.alternativeSubject?.complete()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
<div class="btn-group w-100" ngbDropdown role="group">
|
||||||
|
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="dateBefore || dateAfter ? 'btn-primary' : 'btn-outline-primary'">
|
||||||
|
{{title}}
|
||||||
|
<app-clearable-badge [selected]="isActive" (cleared)="reset()"></app-clearable-badge><span class="visually-hidden">selected</span>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu date-dropdown shadow pt-0" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
<button *ngFor="let rd of relativeDates" class="list-group-item small list-goup list-group-item-action d-flex p-2" role="menuitem" (click)="setRelativeDate(rd.date)">
|
||||||
|
<div _ngcontent-hga-c166="" class="selected-icon me-1">
|
||||||
|
<svg *ngIf="relativeDate === rd.date" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16">
|
||||||
|
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{{rd.name}}
|
||||||
|
</button>
|
||||||
|
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
|
||||||
|
|
||||||
|
<div class="mb-2 d-flex flex-row w-100 justify-content-between small">
|
||||||
|
<div i18n>After</div>
|
||||||
|
<a *ngIf="dateAfter" class="btn btn-link p-0 m-0" (click)="clearAfter()">
|
||||||
|
<svg width="0.8em" height="0.8em" viewBox="0 0 16 16" class="bi bi-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
|
||||||
|
</svg>
|
||||||
|
<small i18n>Clear</small>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||||
|
maxlength="10" [(ngModel)]="dateAfter" ngbDatepicker #dateAfterPicker="ngbDatepicker">
|
||||||
|
<button class="btn btn-outline-secondary" (click)="dateAfterPicker.toggle()" type="button">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="bi bi-calendar" viewBox="0 0 16 16">
|
||||||
|
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
|
||||||
|
|
||||||
|
<div class="mb-2 d-flex flex-row w-100 justify-content-between small">
|
||||||
|
<div i18n>Before</div>
|
||||||
|
<a *ngIf="dateBefore" class="btn btn-link p-0 m-0" (click)="clearBefore()">
|
||||||
|
<svg width="0.8em" height="0.8em" viewBox="0 0 16 16" class="bi bi-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
|
||||||
|
</svg>
|
||||||
|
<small i18n>Clear</small>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||||
|
maxlength="10" [(ngModel)]="dateBefore" ngbDatepicker #dateBeforePicker="ngbDatepicker">
|
||||||
|
<button class="btn btn-outline-secondary" (click)="dateBeforePicker.toggle()" type="button">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="bi bi-calendar" viewBox="0 0 16 16">
|
||||||
|
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,12 @@
|
|||||||
|
.date-dropdown {
|
||||||
|
min-width: 250px;
|
||||||
|
|
||||||
|
.btn-link {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-icon {
|
||||||
|
min-width: 1em;
|
||||||
|
min-height: 1em;
|
||||||
|
}
|
@ -0,0 +1,158 @@
|
|||||||
|
import { formatDate } from '@angular/common'
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
Output,
|
||||||
|
OnInit,
|
||||||
|
OnDestroy,
|
||||||
|
} from '@angular/core'
|
||||||
|
import { NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { Subject, Subscription } from 'rxjs'
|
||||||
|
import { debounceTime } from 'rxjs/operators'
|
||||||
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
|
||||||
|
|
||||||
|
export interface DateSelection {
|
||||||
|
before?: string
|
||||||
|
after?: string
|
||||||
|
relativeDateID?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum RelativeDate {
|
||||||
|
LAST_7_DAYS = 0,
|
||||||
|
LAST_MONTH = 1,
|
||||||
|
LAST_3_MONTHS = 2,
|
||||||
|
LAST_YEAR = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-date-dropdown',
|
||||||
|
templateUrl: './date-dropdown.component.html',
|
||||||
|
styleUrls: ['./date-dropdown.component.scss'],
|
||||||
|
providers: [{ provide: NgbDateAdapter, useClass: ISODateAdapter }],
|
||||||
|
})
|
||||||
|
export class DateDropdownComponent implements OnInit, OnDestroy {
|
||||||
|
constructor(settings: SettingsService) {
|
||||||
|
this.datePlaceHolder = settings.getLocalizedDateInputFormat()
|
||||||
|
}
|
||||||
|
|
||||||
|
relativeDates = [
|
||||||
|
{
|
||||||
|
date: RelativeDate.LAST_7_DAYS,
|
||||||
|
name: $localize`Last 7 days`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: RelativeDate.LAST_MONTH,
|
||||||
|
name: $localize`Last month`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: RelativeDate.LAST_3_MONTHS,
|
||||||
|
name: $localize`Last 3 months`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: RelativeDate.LAST_YEAR,
|
||||||
|
name: $localize`Last year`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
datePlaceHolder: string
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
dateBefore: string
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
dateBeforeChange = new EventEmitter<string>()
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
dateAfter: string
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
dateAfterChange = new EventEmitter<string>()
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
relativeDate: RelativeDate
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
relativeDateChange = new EventEmitter<number>()
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
title: string
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
datesSet = new EventEmitter<DateSelection>()
|
||||||
|
|
||||||
|
get isActive(): boolean {
|
||||||
|
return (
|
||||||
|
this.relativeDate !== null ||
|
||||||
|
this.dateAfter?.length > 0 ||
|
||||||
|
this.dateBefore?.length > 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private datesSetDebounce$ = new Subject()
|
||||||
|
|
||||||
|
private sub: Subscription
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.sub = this.datesSetDebounce$.pipe(debounceTime(400)).subscribe(() => {
|
||||||
|
this.onChange()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
if (this.sub) {
|
||||||
|
this.sub.unsubscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.dateBefore = null
|
||||||
|
this.dateAfter = null
|
||||||
|
this.relativeDate = null
|
||||||
|
this.onChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
setRelativeDate(rd: RelativeDate) {
|
||||||
|
this.dateBefore = null
|
||||||
|
this.dateAfter = null
|
||||||
|
this.relativeDate = this.relativeDate == rd ? null : rd
|
||||||
|
this.onChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange() {
|
||||||
|
this.dateBeforeChange.emit(this.dateBefore)
|
||||||
|
this.dateAfterChange.emit(this.dateAfter)
|
||||||
|
this.relativeDateChange.emit(this.relativeDate)
|
||||||
|
this.datesSet.emit({
|
||||||
|
after: this.dateAfter,
|
||||||
|
before: this.dateBefore,
|
||||||
|
relativeDateID: this.relativeDate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeDebounce() {
|
||||||
|
this.relativeDate = null
|
||||||
|
this.datesSetDebounce$.next({
|
||||||
|
after: this.dateAfter,
|
||||||
|
before: this.dateBefore,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
clearBefore() {
|
||||||
|
this.dateBefore = null
|
||||||
|
this.onChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
clearAfter() {
|
||||||
|
this.dateAfter = null
|
||||||
|
this.onChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
// prevent chars other than numbers and separators
|
||||||
|
onKeyPress(event: KeyboardEvent) {
|
||||||
|
if ('Enter' !== event.key && !/[0-9,\.\/-]+/.test(event.key)) {
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
<form [formGroup]="objectForm" (ngSubmit)="save()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||||
|
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
||||||
|
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
||||||
|
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text>
|
||||||
|
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive" novalidate></app-input-check>
|
||||||
|
|
||||||
|
<div *appIfOwner="object">
|
||||||
|
<app-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></app-permissions-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
@ -0,0 +1,41 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { FormControl, FormGroup } from '@angular/forms'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||||
|
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||||
|
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
||||||
|
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||||
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-correspondent-edit-dialog',
|
||||||
|
templateUrl: './correspondent-edit-dialog.component.html',
|
||||||
|
styleUrls: ['./correspondent-edit-dialog.component.scss'],
|
||||||
|
})
|
||||||
|
export class CorrespondentEditDialogComponent extends EditDialogComponent<PaperlessCorrespondent> {
|
||||||
|
constructor(
|
||||||
|
service: CorrespondentService,
|
||||||
|
activeModal: NgbActiveModal,
|
||||||
|
userService: UserService
|
||||||
|
) {
|
||||||
|
super(service, activeModal, userService)
|
||||||
|
}
|
||||||
|
|
||||||
|
getCreateTitle() {
|
||||||
|
return $localize`Create new correspondent`
|
||||||
|
}
|
||||||
|
|
||||||
|
getEditTitle() {
|
||||||
|
return $localize`Edit correspondent`
|
||||||
|
}
|
||||||
|
|
||||||
|
getForm(): FormGroup {
|
||||||
|
return new FormGroup({
|
||||||
|
name: new FormControl(''),
|
||||||
|
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
||||||
|
match: new FormControl(''),
|
||||||
|
is_insensitive: new FormControl(true),
|
||||||
|
permissions_form: new FormControl(null),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
<form [formGroup]="objectForm" (ngSubmit)="save()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||||
|
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
||||||
|
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
||||||
|
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text>
|
||||||
|
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *appIfOwner="object">
|
||||||
|
<app-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></app-permissions-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
@ -0,0 +1,41 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { FormControl, FormGroup } from '@angular/forms'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||||
|
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||||
|
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
|
||||||
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||||
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-document-type-edit-dialog',
|
||||||
|
templateUrl: './document-type-edit-dialog.component.html',
|
||||||
|
styleUrls: ['./document-type-edit-dialog.component.scss'],
|
||||||
|
})
|
||||||
|
export class DocumentTypeEditDialogComponent extends EditDialogComponent<PaperlessDocumentType> {
|
||||||
|
constructor(
|
||||||
|
service: DocumentTypeService,
|
||||||
|
activeModal: NgbActiveModal,
|
||||||
|
userService: UserService
|
||||||
|
) {
|
||||||
|
super(service, activeModal, userService)
|
||||||
|
}
|
||||||
|
|
||||||
|
getCreateTitle() {
|
||||||
|
return $localize`Create new document type`
|
||||||
|
}
|
||||||
|
|
||||||
|
getEditTitle() {
|
||||||
|
return $localize`Edit document type`
|
||||||
|
}
|
||||||
|
|
||||||
|
getForm(): FormGroup {
|
||||||
|
return new FormGroup({
|
||||||
|
name: new FormControl(''),
|
||||||
|
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
||||||
|
match: new FormControl(''),
|
||||||
|
is_insensitive: new FormControl(true),
|
||||||
|
permissions_form: new FormControl(null),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,143 @@
|
|||||||
|
import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core'
|
||||||
|
import { FormGroup } from '@angular/forms'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { Observable } from 'rxjs'
|
||||||
|
import {
|
||||||
|
MATCHING_ALGORITHMS,
|
||||||
|
MATCH_AUTO,
|
||||||
|
MATCH_NONE,
|
||||||
|
} from 'src/app/data/matching-model'
|
||||||
|
import { ObjectWithId } from 'src/app/data/object-with-id'
|
||||||
|
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
|
||||||
|
import { PaperlessUser } from 'src/app/data/paperless-user'
|
||||||
|
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'
|
||||||
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
|
import { PermissionsFormObject } from '../input/permissions/permissions-form/permissions-form.component'
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export abstract class EditDialogComponent<
|
||||||
|
T extends ObjectWithPermissions | ObjectWithId
|
||||||
|
> implements OnInit
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
protected service: AbstractPaperlessService<T>,
|
||||||
|
private activeModal: NgbActiveModal,
|
||||||
|
private userService: UserService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
users: PaperlessUser[]
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
dialogMode: string = 'create'
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
object: T
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
succeeded = new EventEmitter()
|
||||||
|
|
||||||
|
networkActive = false
|
||||||
|
|
||||||
|
closeEnabled = false
|
||||||
|
|
||||||
|
error = null
|
||||||
|
|
||||||
|
abstract getForm(): FormGroup
|
||||||
|
|
||||||
|
objectForm: FormGroup = this.getForm()
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
if (this.object != null) {
|
||||||
|
if (this.object['permissions']) {
|
||||||
|
this.object['set_permissions'] = this.object['permissions']
|
||||||
|
}
|
||||||
|
|
||||||
|
this.object['permissions_form'] = {
|
||||||
|
owner: (this.object as ObjectWithPermissions).owner,
|
||||||
|
set_permissions: (this.object as ObjectWithPermissions).permissions,
|
||||||
|
}
|
||||||
|
this.objectForm.patchValue(this.object)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait to enable close button so it doesnt steal focus from input since its the first clickable element in the DOM
|
||||||
|
setTimeout(() => {
|
||||||
|
this.closeEnabled = true
|
||||||
|
})
|
||||||
|
|
||||||
|
this.userService.listAll().subscribe((r) => (this.users = r.results))
|
||||||
|
}
|
||||||
|
|
||||||
|
getCreateTitle() {
|
||||||
|
return $localize`Create new item`
|
||||||
|
}
|
||||||
|
|
||||||
|
getEditTitle() {
|
||||||
|
return $localize`Edit item`
|
||||||
|
}
|
||||||
|
|
||||||
|
getSaveErrorMessage(error: string) {
|
||||||
|
return $localize`Could not save element: ${error}`
|
||||||
|
}
|
||||||
|
|
||||||
|
getTitle() {
|
||||||
|
switch (this.dialogMode) {
|
||||||
|
case 'create':
|
||||||
|
return this.getCreateTitle()
|
||||||
|
case 'edit':
|
||||||
|
return this.getEditTitle()
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getMatchingAlgorithms() {
|
||||||
|
return MATCHING_ALGORITHMS
|
||||||
|
}
|
||||||
|
|
||||||
|
get patternRequired(): boolean {
|
||||||
|
return (
|
||||||
|
this.objectForm?.value.matching_algorithm !== MATCH_AUTO &&
|
||||||
|
this.objectForm?.value.matching_algorithm !== MATCH_NONE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
this.error = null
|
||||||
|
const formValues = Object.assign({}, this.objectForm.value)
|
||||||
|
const permissionsObject: PermissionsFormObject =
|
||||||
|
this.objectForm.get('permissions_form')?.value
|
||||||
|
if (permissionsObject) {
|
||||||
|
formValues.owner = permissionsObject.owner
|
||||||
|
formValues.set_permissions = permissionsObject.set_permissions
|
||||||
|
delete formValues.permissions_form
|
||||||
|
}
|
||||||
|
|
||||||
|
var newObject = Object.assign(Object.assign({}, this.object), formValues)
|
||||||
|
var serverResponse: Observable<T>
|
||||||
|
switch (this.dialogMode) {
|
||||||
|
case 'create':
|
||||||
|
serverResponse = this.service.create(newObject)
|
||||||
|
break
|
||||||
|
case 'edit':
|
||||||
|
serverResponse = this.service.update(newObject)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
this.networkActive = true
|
||||||
|
serverResponse.subscribe({
|
||||||
|
next: (result) => {
|
||||||
|
this.activeModal.close()
|
||||||
|
this.succeeded.emit(result)
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
this.error = error.error
|
||||||
|
this.networkActive = false
|
||||||
|
this.succeeded.next(error)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.activeModal.close()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
<form [formGroup]="objectForm" (ngSubmit)="save()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||||
|
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
||||||
|
<app-permissions-select i18n-title title="Permissions" formControlName="permissions" [error]="error?.permissions"></app-permissions-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
@ -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 { PaperlessGroup } from 'src/app/data/paperless-group'
|
||||||
|
import { GroupService } from 'src/app/services/rest/group.service'
|
||||||
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-group-edit-dialog',
|
||||||
|
templateUrl: './group-edit-dialog.component.html',
|
||||||
|
styleUrls: ['./group-edit-dialog.component.scss'],
|
||||||
|
})
|
||||||
|
export class GroupEditDialogComponent extends EditDialogComponent<PaperlessGroup> {
|
||||||
|
constructor(
|
||||||
|
service: GroupService,
|
||||||
|
activeModal: NgbActiveModal,
|
||||||
|
userService: UserService
|
||||||
|
) {
|
||||||
|
super(service, activeModal, userService)
|
||||||
|
}
|
||||||
|
|
||||||
|
getCreateTitle() {
|
||||||
|
return $localize`Create new user group`
|
||||||
|
}
|
||||||
|
|
||||||
|
getEditTitle() {
|
||||||
|
return $localize`Edit user group`
|
||||||
|
}
|
||||||
|
|
||||||
|
getForm(): FormGroup {
|
||||||
|
return new FormGroup({
|
||||||
|
name: new FormControl(''),
|
||||||
|
permissions: new FormControl(null),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
<form [formGroup]="objectForm" (ngSubmit)="save()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||||
|
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
||||||
|
<app-input-text i18n-title title="IMAP Server" formControlName="imap_server" [error]="error?.imap_server"></app-input-text>
|
||||||
|
<app-input-text i18n-title title="IMAP Port" formControlName="imap_port" [error]="error?.imap_port"></app-input-text>
|
||||||
|
<app-input-select i18n-title title="IMAP Security" [items]="imapSecurityOptions" formControlName="imap_security"></app-input-select>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<app-input-text i18n-title title="Username" formControlName="username" [error]="error?.username"></app-input-text>
|
||||||
|
<app-input-password i18n-title title="Password" formControlName="password" [error]="error?.password"></app-input-password>
|
||||||
|
<app-input-check i18n-title title="Password is token" i18n-hint hint="Check if the password above is a token used for authentication" formControlName="is_token" [error]="error?.is_token"></app-input-check>
|
||||||
|
<app-input-text i18n-title title="Character Set" formControlName="character_set" [error]="error?.character_set"></app-input-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div class="m-0 me-2">
|
||||||
|
<ngb-alert #testResultAlert *ngIf="testResult" [type]="testResult" class="mb-0 py-2" (closed)="testResult = null">{{testResultMessage}}</ngb-alert>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-outline-primary" (click)="test()" [disabled]="networkActive || testActive">
|
||||||
|
<ng-container *ngIf="testActive">
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
|
<span class="visually-hidden mr-1" i18n>Loading...</span>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container i18n>Test</ng-container>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
@ -0,0 +1,4 @@
|
|||||||
|
::ng-deep .alert-dismissible .btn-close {
|
||||||
|
padding-top: 0.75rem !important;
|
||||||
|
padding-bottom: 0.75rem !important;
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
import { Component, ViewChild } from '@angular/core'
|
||||||
|
import { FormControl, FormGroup } from '@angular/forms'
|
||||||
|
import { NgbActiveModal, NgbAlert } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||||
|
import {
|
||||||
|
IMAPSecurity,
|
||||||
|
PaperlessMailAccount,
|
||||||
|
} from 'src/app/data/paperless-mail-account'
|
||||||
|
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
|
||||||
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
|
|
||||||
|
const IMAP_SECURITY_OPTIONS = [
|
||||||
|
{ id: IMAPSecurity.None, name: $localize`No encryption` },
|
||||||
|
{ id: IMAPSecurity.SSL, name: $localize`SSL` },
|
||||||
|
{ id: IMAPSecurity.STARTTLS, name: $localize`STARTTLS` },
|
||||||
|
]
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-mail-account-edit-dialog',
|
||||||
|
templateUrl: './mail-account-edit-dialog.component.html',
|
||||||
|
styleUrls: ['./mail-account-edit-dialog.component.scss'],
|
||||||
|
})
|
||||||
|
export class MailAccountEditDialogComponent extends EditDialogComponent<PaperlessMailAccount> {
|
||||||
|
testActive: boolean = false
|
||||||
|
testResult: string
|
||||||
|
alertTimeout
|
||||||
|
|
||||||
|
@ViewChild('testResultAlert', { static: false }) testResultAlert: NgbAlert
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
service: MailAccountService,
|
||||||
|
activeModal: NgbActiveModal,
|
||||||
|
userService: UserService
|
||||||
|
) {
|
||||||
|
super(service, activeModal, userService)
|
||||||
|
}
|
||||||
|
|
||||||
|
getCreateTitle() {
|
||||||
|
return $localize`Create new mail account`
|
||||||
|
}
|
||||||
|
|
||||||
|
getEditTitle() {
|
||||||
|
return $localize`Edit mail account`
|
||||||
|
}
|
||||||
|
|
||||||
|
getForm(): FormGroup {
|
||||||
|
return new FormGroup({
|
||||||
|
name: new FormControl(null),
|
||||||
|
imap_server: new FormControl(null),
|
||||||
|
imap_port: new FormControl(null),
|
||||||
|
imap_security: new FormControl(IMAPSecurity.SSL),
|
||||||
|
username: new FormControl(null),
|
||||||
|
password: new FormControl(null),
|
||||||
|
is_token: new FormControl(false),
|
||||||
|
character_set: new FormControl('UTF-8'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get imapSecurityOptions() {
|
||||||
|
return IMAP_SECURITY_OPTIONS
|
||||||
|
}
|
||||||
|
|
||||||
|
test() {
|
||||||
|
this.testActive = true
|
||||||
|
this.testResult = null
|
||||||
|
clearTimeout(this.alertTimeout)
|
||||||
|
const mailService = this.service as MailAccountService
|
||||||
|
const newObject = Object.assign(
|
||||||
|
Object.assign({}, this.object),
|
||||||
|
this.objectForm.value
|
||||||
|
)
|
||||||
|
mailService.test(newObject).subscribe({
|
||||||
|
next: (result: { success: boolean }) => {
|
||||||
|
this.testActive = false
|
||||||
|
this.testResult = result.success ? 'success' : 'danger'
|
||||||
|
this.alertTimeout = setTimeout(() => this.testResultAlert.close(), 5000)
|
||||||
|
},
|
||||||
|
error: (e) => {
|
||||||
|
this.testActive = false
|
||||||
|
this.testResult = 'danger'
|
||||||
|
this.alertTimeout = setTimeout(() => this.testResultAlert.close(), 5000)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get testResultMessage() {
|
||||||
|
return this.testResult === 'success'
|
||||||
|
? $localize`Successfully connected to the mail server`
|
||||||
|
: $localize`Unable to connect to the mail server`
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
<form [formGroup]="objectForm" (ngSubmit)="save()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||||
|
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
||||||
|
<app-input-select i18n-title title="Account" [items]="accounts" formControlName="account"></app-input-select>
|
||||||
|
<app-input-text i18n-title title="Folder" formControlName="folder" i18n-hint hint="Subfolders must be separated by a delimiter, often a dot ('.') or slash ('/'), but it varies by mail server." [error]="error?.folder"></app-input-text>
|
||||||
|
<app-input-number i18n-title title="Maximum age (days)" formControlName="maximum_age" [showAdd]="false" [error]="error?.maximum_age"></app-input-number>
|
||||||
|
<app-input-select i18n-title title="Attachment type" [items]="attachmentTypeOptions" formControlName="attachment_type"></app-input-select>
|
||||||
|
<app-input-select i18n-title title="Consumption scope" [items]="consumptionScopeOptions" formControlName="consumption_scope" i18n-hint hint="See docs for .eml processing requirements"></app-input-select>
|
||||||
|
<app-input-number i18n-title title="Rule order" formControlName="order" [showAdd]="false" [error]="error?.order"></app-input-number>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<p class="small" i18n>Paperless will only process mails that match <em>all</em> of the filters specified below.</p>
|
||||||
|
<app-input-text i18n-title title="Filter from" formControlName="filter_from" [error]="error?.filter_from"></app-input-text>
|
||||||
|
<app-input-text i18n-title title="Filter to" formControlName="filter_to" [error]="error?.filter_to"></app-input-text>
|
||||||
|
<app-input-text i18n-title title="Filter subject" formControlName="filter_subject" [error]="error?.filter_subject"></app-input-text>
|
||||||
|
<app-input-text i18n-title title="Filter body" formControlName="filter_body" [error]="error?.filter_body"></app-input-text>
|
||||||
|
<app-input-text i18n-title title="Filter attachment filename" formControlName="filter_attachment_filename" i18n-hint hint="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_attachment_filename"></app-input-text>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<app-input-select i18n-title title="Action" [items]="actionOptions" formControlName="action" i18n-hint hint="Action is only performed when documents are consumed from the mail. Mails without attachments remain entirely untouched."></app-input-select>
|
||||||
|
<app-input-text i18n-title title="Action parameter" *ngIf="showActionParamField" formControlName="action_parameter" [error]="error?.action_parameter"></app-input-text>
|
||||||
|
<app-input-select i18n-title title="Assign title from" [items]="metadataTitleOptions" formControlName="assign_title_from"></app-input-select>
|
||||||
|
<app-input-tags [allowCreate]="false" formControlName="assign_tags"></app-input-tags>
|
||||||
|
<app-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></app-input-select>
|
||||||
|
<app-input-select i18n-title title="Assign correspondent from" [items]="metadataCorrespondentOptions" formControlName="assign_correspondent_from"></app-input-select>
|
||||||
|
<app-input-select *ngIf="showCorrespondentField" i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></app-input-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<span class="text-danger" *ngIf="error?.non_field_errors"><ng-container i18n>Error</ng-container>: {{error.non_field_errors}}</span>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
@ -0,0 +1,205 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { FormControl, FormGroup } from '@angular/forms'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { first } from 'rxjs'
|
||||||
|
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||||
|
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
||||||
|
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
|
||||||
|
import { PaperlessMailAccount } from 'src/app/data/paperless-mail-account'
|
||||||
|
import {
|
||||||
|
MailAction,
|
||||||
|
MailFilterAttachmentType,
|
||||||
|
MailMetadataCorrespondentOption,
|
||||||
|
MailMetadataTitleOption,
|
||||||
|
PaperlessMailRule,
|
||||||
|
MailRuleConsumptionScope,
|
||||||
|
} from 'src/app/data/paperless-mail-rule'
|
||||||
|
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||||
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||||
|
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
|
||||||
|
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
|
||||||
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
|
|
||||||
|
const ATTACHMENT_TYPE_OPTIONS = [
|
||||||
|
{
|
||||||
|
id: MailFilterAttachmentType.Attachments,
|
||||||
|
name: $localize`Only process attachments`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: MailFilterAttachmentType.Everything,
|
||||||
|
name: $localize`Process all files, including 'inline' attachments`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const CONSUMPTION_SCOPE_OPTIONS = [
|
||||||
|
{
|
||||||
|
id: MailRuleConsumptionScope.Attachments,
|
||||||
|
name: $localize`Only process attachments`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: MailRuleConsumptionScope.Email_Only,
|
||||||
|
name: $localize`Process message as .eml`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: MailRuleConsumptionScope.Everything,
|
||||||
|
name: $localize`Process message as .eml and attachments separately`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const ACTION_OPTIONS = [
|
||||||
|
{
|
||||||
|
id: MailAction.Delete,
|
||||||
|
name: $localize`Delete`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: MailAction.Move,
|
||||||
|
name: $localize`Move to specified folder`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: MailAction.MarkRead,
|
||||||
|
name: $localize`Mark as read, don't process read mails`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: MailAction.Flag,
|
||||||
|
name: $localize`Flag the mail, don't process flagged mails`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: MailAction.Tag,
|
||||||
|
name: $localize`Tag the mail with specified tag, don't process tagged mails`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const METADATA_TITLE_OPTIONS = [
|
||||||
|
{
|
||||||
|
id: MailMetadataTitleOption.FromSubject,
|
||||||
|
name: $localize`Use subject as title`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: MailMetadataTitleOption.FromFilename,
|
||||||
|
name: $localize`Use attachment filename as title`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const METADATA_CORRESPONDENT_OPTIONS = [
|
||||||
|
{
|
||||||
|
id: MailMetadataCorrespondentOption.FromNothing,
|
||||||
|
name: $localize`Do not assign a correspondent`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: MailMetadataCorrespondentOption.FromEmail,
|
||||||
|
name: $localize`Use mail address`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: MailMetadataCorrespondentOption.FromName,
|
||||||
|
name: $localize`Use name (or mail address if not available)`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: MailMetadataCorrespondentOption.FromCustom,
|
||||||
|
name: $localize`Use correspondent selected below`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-mail-rule-edit-dialog',
|
||||||
|
templateUrl: './mail-rule-edit-dialog.component.html',
|
||||||
|
styleUrls: ['./mail-rule-edit-dialog.component.scss'],
|
||||||
|
})
|
||||||
|
export class MailRuleEditDialogComponent extends EditDialogComponent<PaperlessMailRule> {
|
||||||
|
accounts: PaperlessMailAccount[]
|
||||||
|
correspondents: PaperlessCorrespondent[]
|
||||||
|
documentTypes: PaperlessDocumentType[]
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
service: MailRuleService,
|
||||||
|
activeModal: NgbActiveModal,
|
||||||
|
accountService: MailAccountService,
|
||||||
|
correspondentService: CorrespondentService,
|
||||||
|
documentTypeService: DocumentTypeService,
|
||||||
|
userService: UserService
|
||||||
|
) {
|
||||||
|
super(service, activeModal, userService)
|
||||||
|
|
||||||
|
accountService
|
||||||
|
.listAll()
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe((result) => (this.accounts = result.results))
|
||||||
|
|
||||||
|
correspondentService
|
||||||
|
.listAll()
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe((result) => (this.correspondents = result.results))
|
||||||
|
|
||||||
|
documentTypeService
|
||||||
|
.listAll()
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe((result) => (this.documentTypes = result.results))
|
||||||
|
}
|
||||||
|
|
||||||
|
getCreateTitle() {
|
||||||
|
return $localize`Create new mail rule`
|
||||||
|
}
|
||||||
|
|
||||||
|
getEditTitle() {
|
||||||
|
return $localize`Edit mail rule`
|
||||||
|
}
|
||||||
|
|
||||||
|
getForm(): FormGroup {
|
||||||
|
return new FormGroup({
|
||||||
|
name: new FormControl(null),
|
||||||
|
account: new FormControl(null),
|
||||||
|
folder: new FormControl('INBOX'),
|
||||||
|
filter_from: new FormControl(null),
|
||||||
|
filter_to: new FormControl(null),
|
||||||
|
filter_subject: new FormControl(null),
|
||||||
|
filter_body: new FormControl(null),
|
||||||
|
filter_attachment_filename: new FormControl(null),
|
||||||
|
maximum_age: new FormControl(null),
|
||||||
|
attachment_type: new FormControl(MailFilterAttachmentType.Attachments),
|
||||||
|
consumption_scope: new FormControl(MailRuleConsumptionScope.Attachments),
|
||||||
|
order: new FormControl(null),
|
||||||
|
action: new FormControl(MailAction.MarkRead),
|
||||||
|
action_parameter: new FormControl(null),
|
||||||
|
assign_title_from: new FormControl(MailMetadataTitleOption.FromSubject),
|
||||||
|
assign_tags: new FormControl([]),
|
||||||
|
assign_document_type: new FormControl(null),
|
||||||
|
assign_correspondent_from: new FormControl(
|
||||||
|
MailMetadataCorrespondentOption.FromNothing
|
||||||
|
),
|
||||||
|
assign_correspondent: new FormControl(null),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get showCorrespondentField(): boolean {
|
||||||
|
return (
|
||||||
|
this.objectForm?.get('assign_correspondent_from')?.value ==
|
||||||
|
MailMetadataCorrespondentOption.FromCustom
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
get showActionParamField(): boolean {
|
||||||
|
return (
|
||||||
|
this.objectForm?.get('action')?.value == MailAction.Move ||
|
||||||
|
this.objectForm?.get('action')?.value == MailAction.Tag
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
get attachmentTypeOptions() {
|
||||||
|
return ATTACHMENT_TYPE_OPTIONS
|
||||||
|
}
|
||||||
|
|
||||||
|
get actionOptions() {
|
||||||
|
return ACTION_OPTIONS
|
||||||
|
}
|
||||||
|
|
||||||
|
get metadataTitleOptions() {
|
||||||
|
return METADATA_TITLE_OPTIONS
|
||||||
|
}
|
||||||
|
|
||||||
|
get metadataCorrespondentOptions() {
|
||||||
|
return METADATA_CORRESPONDENT_OPTIONS
|
||||||
|
}
|
||||||
|
|
||||||
|
get consumptionScopeOptions() {
|
||||||
|
return CONSUMPTION_SCOPE_OPTIONS
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
<form [formGroup]="objectForm" (ngSubmit)="save()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||||
|
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
||||||
|
<app-input-text i18n-title title="Path" formControlName="path" [error]="error?.path" [hint]="pathHint"></app-input-text>
|
||||||
|
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
||||||
|
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text>
|
||||||
|
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check>
|
||||||
|
|
||||||
|
<div *appIfOwner="object">
|
||||||
|
<app-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></app-permissions-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
@ -0,0 +1,52 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { FormControl, FormGroup } from '@angular/forms'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||||
|
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||||
|
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
||||||
|
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||||
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-storage-path-edit-dialog',
|
||||||
|
templateUrl: './storage-path-edit-dialog.component.html',
|
||||||
|
styleUrls: ['./storage-path-edit-dialog.component.scss'],
|
||||||
|
})
|
||||||
|
export class StoragePathEditDialogComponent extends EditDialogComponent<PaperlessStoragePath> {
|
||||||
|
constructor(
|
||||||
|
service: StoragePathService,
|
||||||
|
activeModal: NgbActiveModal,
|
||||||
|
userService: UserService
|
||||||
|
) {
|
||||||
|
super(service, activeModal, userService)
|
||||||
|
}
|
||||||
|
|
||||||
|
get pathHint() {
|
||||||
|
return (
|
||||||
|
$localize`e.g.` +
|
||||||
|
' <code>{created_year}-{title}</code> ' +
|
||||||
|
$localize`or use slashes to add directories e.g.` +
|
||||||
|
' <code>{created_year}/{correspondent}/{title}</code>. ' +
|
||||||
|
$localize`See <a target="_blank" href="https://docs.paperless-ngx.com/advanced_usage/#file-name-handling">documentation</a> for full list.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getCreateTitle() {
|
||||||
|
return $localize`Create new storage path`
|
||||||
|
}
|
||||||
|
|
||||||
|
getEditTitle() {
|
||||||
|
return $localize`Edit storage path`
|
||||||
|
}
|
||||||
|
|
||||||
|
getForm(): FormGroup {
|
||||||
|
return new FormGroup({
|
||||||
|
name: new FormControl(''),
|
||||||
|
path: new FormControl(''),
|
||||||
|
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
||||||
|
match: new FormControl(''),
|
||||||
|
is_insensitive: new FormControl(true),
|
||||||
|
permissions_form: new FormControl(null),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
<form [formGroup]="objectForm" (ngSubmit)="save()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||||
|
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
||||||
|
|
||||||
|
<app-input-color i18n-title title="Color" formControlName="color" [error]="error?.color"></app-input-color>
|
||||||
|
|
||||||
|
<app-input-check i18n-title title="Inbox tag" formControlName="is_inbox_tag" i18n-hint hint="Inbox tags are automatically assigned to all consumed documents."></app-input-check>
|
||||||
|
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
||||||
|
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text>
|
||||||
|
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check>
|
||||||
|
|
||||||
|
<div *appIfOwner="object">
|
||||||
|
<app-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></app-permissions-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
@ -0,0 +1,44 @@
|
|||||||
|
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 { randomColor } from 'src/app/utils/color'
|
||||||
|
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||||
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-tag-edit-dialog',
|
||||||
|
templateUrl: './tag-edit-dialog.component.html',
|
||||||
|
styleUrls: ['./tag-edit-dialog.component.scss'],
|
||||||
|
})
|
||||||
|
export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> {
|
||||||
|
constructor(
|
||||||
|
service: TagService,
|
||||||
|
activeModal: NgbActiveModal,
|
||||||
|
userService: UserService
|
||||||
|
) {
|
||||||
|
super(service, activeModal, userService)
|
||||||
|
}
|
||||||
|
|
||||||
|
getCreateTitle() {
|
||||||
|
return $localize`Create new tag`
|
||||||
|
}
|
||||||
|
|
||||||
|
getEditTitle() {
|
||||||
|
return $localize`Edit tag`
|
||||||
|
}
|
||||||
|
|
||||||
|
getForm(): FormGroup {
|
||||||
|
return new FormGroup({
|
||||||
|
name: new FormControl(''),
|
||||||
|
color: new FormControl(randomColor()),
|
||||||
|
is_inbox_tag: new FormControl(false),
|
||||||
|
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
||||||
|
match: new FormControl(''),
|
||||||
|
is_insensitive: new FormControl(true),
|
||||||
|
permissions_form: new FormControl(null),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
<form [formGroup]="objectForm" (ngSubmit)="save()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||||
|
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<app-input-text i18n-title title="Username" formControlName="username" [error]="error?.username"></app-input-text>
|
||||||
|
<app-input-text i18n-title title="Email" formControlName="email" [error]="error?.email"></app-input-text>
|
||||||
|
<app-input-password i18n-title title="Password" formControlName="password" [error]="error?.password"></app-input-password>
|
||||||
|
<app-input-text i18n-title title="First name" formControlName="first_name" [error]="error?.first_name"></app-input-text>
|
||||||
|
<app-input-text i18n-title title="Last name" formControlName="last_name" [error]="error?.first_name"></app-input-text>
|
||||||
|
|
||||||
|
<div class="mb-2">
|
||||||
|
<div class="form-check form-switch form-check-inline">
|
||||||
|
<input type="checkbox" class="form-check-input" id="is_active" formControlName="is_active">
|
||||||
|
<label class="form-check-label" for="is_active" i18n>Active</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-switch form-check-inline">
|
||||||
|
<input type="checkbox" class="form-check-input" id="is_superuser" formControlName="is_superuser" (change)="onToggleSuperUser()">
|
||||||
|
<label class="form-check-label" for="is_superuser"><ng-container i18n>Superuser</ng-container> <small class="form-text text-muted ms-1" i18n>(Grants all permissions and can view objects)</small></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-input-select i18n-title title="Groups" [items]="groups" multiple="true" formControlName="groups"></app-input-select>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<app-permissions-select i18n-title title="Permissions" formControlName="user_permissions" [error]="error?.user_permissions" [inheritedPermissions]="inheritedPermissions"></app-permissions-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
@ -0,0 +1,87 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core'
|
||||||
|
import { FormControl, FormGroup } from '@angular/forms'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { first } from 'rxjs'
|
||||||
|
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||||
|
import { PaperlessGroup } from 'src/app/data/paperless-group'
|
||||||
|
import { PaperlessUser } from 'src/app/data/paperless-user'
|
||||||
|
import { GroupService } from 'src/app/services/rest/group.service'
|
||||||
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-user-edit-dialog',
|
||||||
|
templateUrl: './user-edit-dialog.component.html',
|
||||||
|
styleUrls: ['./user-edit-dialog.component.scss'],
|
||||||
|
})
|
||||||
|
export class UserEditDialogComponent
|
||||||
|
extends EditDialogComponent<PaperlessUser>
|
||||||
|
implements OnInit
|
||||||
|
{
|
||||||
|
groups: PaperlessGroup[]
|
||||||
|
passwordIsSet: boolean = false
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
service: UserService,
|
||||||
|
activeModal: NgbActiveModal,
|
||||||
|
groupsService: GroupService
|
||||||
|
) {
|
||||||
|
super(service, activeModal, service)
|
||||||
|
|
||||||
|
groupsService
|
||||||
|
.listAll()
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe((result) => (this.groups = result.results))
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit()
|
||||||
|
this.onToggleSuperUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
getCreateTitle() {
|
||||||
|
return $localize`Create new user account`
|
||||||
|
}
|
||||||
|
|
||||||
|
getEditTitle() {
|
||||||
|
return $localize`Edit user account`
|
||||||
|
}
|
||||||
|
|
||||||
|
getForm(): FormGroup {
|
||||||
|
return new FormGroup({
|
||||||
|
username: new FormControl(''),
|
||||||
|
email: new FormControl(''),
|
||||||
|
password: new FormControl(null),
|
||||||
|
first_name: new FormControl(''),
|
||||||
|
last_name: new FormControl(''),
|
||||||
|
is_active: new FormControl(true),
|
||||||
|
is_superuser: new FormControl(false),
|
||||||
|
groups: new FormControl([]),
|
||||||
|
user_permissions: new FormControl([]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggleSuperUser() {
|
||||||
|
if (this.objectForm.get('is_superuser').value) {
|
||||||
|
this.objectForm.get('user_permissions').disable()
|
||||||
|
} else {
|
||||||
|
this.objectForm.get('user_permissions').enable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get inheritedPermissions(): string[] {
|
||||||
|
const groupsVal: Array<number> = this.objectForm.get('groups').value
|
||||||
|
|
||||||
|
if (!groupsVal) return []
|
||||||
|
else
|
||||||
|
return groupsVal.flatMap(
|
||||||
|
(id) => this.groups.find((g) => g.id == id)?.permissions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
save(): void {
|
||||||
|
this.passwordIsSet =
|
||||||
|
this.objectForm.get('password').value?.toString().replaceAll('*', '')
|
||||||
|
.length > 0
|
||||||
|
super.save()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
<div class="btn-group w-100" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown">
|
||||||
|
<button class="btn btn-sm" id="dropdown_{{name}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled">
|
||||||
|
<svg class="toolbaricon" fill="currentColor">
|
||||||
|
<use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
|
||||||
|
</svg>
|
||||||
|
<div class="d-none d-sm-inline"> {{title}}</div>
|
||||||
|
<ng-container *ngIf="!editing && selectionModel.totalCount > 0">
|
||||||
|
<app-clearable-badge [number]="selectionModel.totalCount" [selected]="selectionModel.selectionSize() > 0" (cleared)="reset()"></app-clearable-badge>
|
||||||
|
</ng-container>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown_{{name}}">
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
<div *ngIf="!editing && manyToOne" class="list-group-item d-flex">
|
||||||
|
<div class="btn-group btn-group-xs flex-fill" role="group">
|
||||||
|
<input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorAnd_{{name}}" name="logicalOperatorAnd_{{name}}" value="and">
|
||||||
|
<label class="btn btn-outline-primary" for="logicalOperatorAnd_{{name}}" i18n>All</label>
|
||||||
|
<input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorOr_{{name}}" name="logicalOperatorOr_{{name}}" value="or">
|
||||||
|
<label class="btn btn-outline-primary" for="logicalOperatorOr_{{name}}" i18n>Any</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="!editing && !manyToOne" class="list-group-item d-flex">
|
||||||
|
<div class="btn-group btn-group-xs flex-fill" role="group">
|
||||||
|
<input [(ngModel)]="selectionModel.intersection" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleIntersection()" type="radio" class="btn-check" id="intersectionInclude_{{name}}" name="intersectionInclude_{{name}}" value="include">
|
||||||
|
<label class="btn btn-outline-primary" for="intersectionInclude_{{name}}" i18n>Include</label>
|
||||||
|
<input [(ngModel)]="selectionModel.intersection" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleIntersection()" type="radio" class="btn-check" id="intersectionExclude_{{name}}" name="intersectionExclude_{{name}}" value="exclude">
|
||||||
|
<label class="btn btn-outline-primary" for="intersectionExclude_{{name}}" i18n>Exclude</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<input class="form-control" type="text" [(ngModel)]="filterText" [placeholder]="filterPlaceholder" (keyup.enter)="listFilterEnter()" #listFilterTextInput>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="selectionModel.items" class="items">
|
||||||
|
<ng-container *ngFor="let item of selectionModel.itemsSorted | filter: filterText">
|
||||||
|
<app-toggleable-dropdown-button *ngIf="allowSelectNone || item.id" [item]="item" [state]="selectionModel.get(item.id)" [count]="getUpdatedDocumentCount(item.id)" (toggle)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)" [disabled]="disabled"></app-toggleable-dropdown-button>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
<button *ngIf="editing" class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!modelIsDirty || disabled">
|
||||||
|
<small class="ms-2" [ngClass]="{'fw-bold': modelIsDirty}" i18n>Apply</small>
|
||||||
|
<svg width="1.5em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#arrow-right" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div *ngIf="!editing && manyToOne" class="list-group-item list-group-item-note pt-1 pb-2">
|
||||||
|
<small i18n>Click again to exclude items.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,42 @@
|
|||||||
|
.badge-corner {
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
right: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
min-width: 250px;
|
||||||
|
|
||||||
|
.items {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group > label.disabled {
|
||||||
|
filter: brightness(0.5);
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: var(--pngx-primary-lighten-30);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
small > svg {
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item-note {
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 65%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.show .btn-outline-primary {
|
||||||
|
color: #fff;
|
||||||
|
}
|
@ -0,0 +1,484 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
Output,
|
||||||
|
ElementRef,
|
||||||
|
ViewChild,
|
||||||
|
} from '@angular/core'
|
||||||
|
import { FilterPipe } from 'src/app/pipes/filter.pipe'
|
||||||
|
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { ToggleableItemState } from './toggleable-dropdown-button/toggleable-dropdown-button.component'
|
||||||
|
import { MatchingModel } from 'src/app/data/matching-model'
|
||||||
|
import { Subject } from 'rxjs'
|
||||||
|
import { SelectionDataItem } from 'src/app/services/rest/document.service'
|
||||||
|
|
||||||
|
export interface ChangedItems {
|
||||||
|
itemsToAdd: MatchingModel[]
|
||||||
|
itemsToRemove: MatchingModel[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LogicalOperator {
|
||||||
|
And = 'and',
|
||||||
|
Or = 'or',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Intersection {
|
||||||
|
Include = 'include',
|
||||||
|
Exclude = 'exclude',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FilterableDropdownSelectionModel {
|
||||||
|
changed = new Subject<FilterableDropdownSelectionModel>()
|
||||||
|
|
||||||
|
manyToOne = false
|
||||||
|
singleSelect = false
|
||||||
|
private _logicalOperator: LogicalOperator = LogicalOperator.And
|
||||||
|
temporaryLogicalOperator: LogicalOperator = this._logicalOperator
|
||||||
|
private _intersection: Intersection = Intersection.Include
|
||||||
|
temporaryIntersection: Intersection = this._intersection
|
||||||
|
|
||||||
|
items: MatchingModel[] = []
|
||||||
|
|
||||||
|
get itemsSorted(): MatchingModel[] {
|
||||||
|
// TODO: this is getting called very often
|
||||||
|
return this.items.sort((a, b) => {
|
||||||
|
if (a.id == null && b.id != null) {
|
||||||
|
return -1
|
||||||
|
} else if (a.id != null && b.id == null) {
|
||||||
|
return 1
|
||||||
|
} else if (
|
||||||
|
this.getNonTemporary(a.id) == ToggleableItemState.NotSelected &&
|
||||||
|
this.getNonTemporary(b.id) != ToggleableItemState.NotSelected
|
||||||
|
) {
|
||||||
|
return 1
|
||||||
|
} else if (
|
||||||
|
this.getNonTemporary(a.id) != ToggleableItemState.NotSelected &&
|
||||||
|
this.getNonTemporary(b.id) == ToggleableItemState.NotSelected
|
||||||
|
) {
|
||||||
|
return -1
|
||||||
|
} else {
|
||||||
|
return a.name.localeCompare(b.name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private selectionStates = new Map<number, ToggleableItemState>()
|
||||||
|
|
||||||
|
private temporarySelectionStates = new Map<number, ToggleableItemState>()
|
||||||
|
|
||||||
|
getSelectedItems() {
|
||||||
|
return this.items.filter(
|
||||||
|
(i) =>
|
||||||
|
this.temporarySelectionStates.get(i.id) == ToggleableItemState.Selected
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getExcludedItems() {
|
||||||
|
return this.items.filter(
|
||||||
|
(i) =>
|
||||||
|
this.temporarySelectionStates.get(i.id) == ToggleableItemState.Excluded
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
set(id: number, state: ToggleableItemState, fireEvent = true) {
|
||||||
|
if (state == ToggleableItemState.NotSelected) {
|
||||||
|
this.temporarySelectionStates.delete(id)
|
||||||
|
} else {
|
||||||
|
this.temporarySelectionStates.set(id, state)
|
||||||
|
}
|
||||||
|
if (fireEvent) {
|
||||||
|
this.changed.next(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle(id: number, fireEvent = true) {
|
||||||
|
let state = this.temporarySelectionStates.get(id)
|
||||||
|
if (
|
||||||
|
state == null ||
|
||||||
|
(state != ToggleableItemState.Selected &&
|
||||||
|
state != ToggleableItemState.Excluded)
|
||||||
|
) {
|
||||||
|
if (this.manyToOne || this.singleSelect) {
|
||||||
|
this.temporarySelectionStates.set(id, ToggleableItemState.Selected)
|
||||||
|
|
||||||
|
if (this.singleSelect) {
|
||||||
|
for (let key of this.temporarySelectionStates.keys()) {
|
||||||
|
if (key != id) {
|
||||||
|
this.temporarySelectionStates.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let newState =
|
||||||
|
this.intersection == Intersection.Include
|
||||||
|
? ToggleableItemState.Selected
|
||||||
|
: ToggleableItemState.Excluded
|
||||||
|
if (!id) newState = ToggleableItemState.Selected
|
||||||
|
if (
|
||||||
|
state == ToggleableItemState.Excluded &&
|
||||||
|
this.intersection == Intersection.Exclude
|
||||||
|
) {
|
||||||
|
newState = ToggleableItemState.NotSelected
|
||||||
|
}
|
||||||
|
this.temporarySelectionStates.set(id, newState)
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
state == ToggleableItemState.Selected ||
|
||||||
|
state == ToggleableItemState.Excluded
|
||||||
|
) {
|
||||||
|
this.temporarySelectionStates.delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
for (let key of this.temporarySelectionStates.keys()) {
|
||||||
|
if (key) {
|
||||||
|
this.temporarySelectionStates.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.temporarySelectionStates.delete(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fireEvent) {
|
||||||
|
this.changed.next(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exclude(id: number, fireEvent: boolean = true) {
|
||||||
|
let state = this.temporarySelectionStates.get(id)
|
||||||
|
if (id && (state == null || state != ToggleableItemState.Excluded)) {
|
||||||
|
this.temporaryLogicalOperator = this._logicalOperator = this.manyToOne
|
||||||
|
? LogicalOperator.And
|
||||||
|
: LogicalOperator.Or
|
||||||
|
|
||||||
|
if (this.manyToOne || this.singleSelect) {
|
||||||
|
this.temporarySelectionStates.set(id, ToggleableItemState.Excluded)
|
||||||
|
|
||||||
|
if (this.singleSelect) {
|
||||||
|
for (let key of this.temporarySelectionStates.keys()) {
|
||||||
|
if (key != id) {
|
||||||
|
this.temporarySelectionStates.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let newState =
|
||||||
|
this.intersection == Intersection.Include
|
||||||
|
? ToggleableItemState.Selected
|
||||||
|
: ToggleableItemState.Excluded
|
||||||
|
if (
|
||||||
|
state == ToggleableItemState.Selected &&
|
||||||
|
this.intersection == Intersection.Include
|
||||||
|
) {
|
||||||
|
newState = ToggleableItemState.NotSelected
|
||||||
|
}
|
||||||
|
this.temporarySelectionStates.set(id, newState)
|
||||||
|
}
|
||||||
|
} else if (!id || state == ToggleableItemState.Excluded) {
|
||||||
|
this.temporarySelectionStates.delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fireEvent) {
|
||||||
|
this.changed.next(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getNonTemporary(id: number) {
|
||||||
|
return this.selectionStates.get(id) || ToggleableItemState.NotSelected
|
||||||
|
}
|
||||||
|
|
||||||
|
get logicalOperator(): LogicalOperator {
|
||||||
|
return this.temporaryLogicalOperator
|
||||||
|
}
|
||||||
|
|
||||||
|
set logicalOperator(operator: LogicalOperator) {
|
||||||
|
this.temporaryLogicalOperator = operator
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleOperator() {
|
||||||
|
this.changed.next(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
get intersection(): Intersection {
|
||||||
|
return this.temporaryIntersection
|
||||||
|
}
|
||||||
|
|
||||||
|
set intersection(intersection: Intersection) {
|
||||||
|
this.temporaryIntersection = intersection
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleIntersection() {
|
||||||
|
if (this.temporarySelectionStates.size === 0) return
|
||||||
|
let newState =
|
||||||
|
this.intersection == Intersection.Include
|
||||||
|
? ToggleableItemState.Selected
|
||||||
|
: ToggleableItemState.Excluded
|
||||||
|
this.temporarySelectionStates.forEach((state, key) => {
|
||||||
|
this.temporarySelectionStates.set(key, newState)
|
||||||
|
})
|
||||||
|
this.changed.next(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
get(id: number) {
|
||||||
|
return (
|
||||||
|
this.temporarySelectionStates.get(id) || ToggleableItemState.NotSelected
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionSize() {
|
||||||
|
return this.getSelectedItems().length
|
||||||
|
}
|
||||||
|
|
||||||
|
get totalCount() {
|
||||||
|
return this.getSelectedItems().length + this.getExcludedItems().length
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(fireEvent = true) {
|
||||||
|
this.temporarySelectionStates.clear()
|
||||||
|
this.temporaryLogicalOperator = this._logicalOperator = LogicalOperator.And
|
||||||
|
this.temporaryIntersection = this._intersection = Intersection.Include
|
||||||
|
if (fireEvent) {
|
||||||
|
this.changed.next(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isDirty() {
|
||||||
|
if (
|
||||||
|
!Array.from(this.temporarySelectionStates.keys()).every(
|
||||||
|
(id) =>
|
||||||
|
this.temporarySelectionStates.get(id) == this.selectionStates.get(id)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
} else if (
|
||||||
|
!Array.from(this.selectionStates.keys()).every(
|
||||||
|
(id) =>
|
||||||
|
this.selectionStates.get(id) == this.temporarySelectionStates.get(id)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
} else if (this.temporaryLogicalOperator !== this._logicalOperator) {
|
||||||
|
return true
|
||||||
|
} else if (this.temporaryIntersection !== this._intersection) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isNoneSelected() {
|
||||||
|
return (
|
||||||
|
this.selectionSize() == 1 &&
|
||||||
|
this.get(null) == ToggleableItemState.Selected
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(map) {
|
||||||
|
this.temporarySelectionStates = map
|
||||||
|
this.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
apply() {
|
||||||
|
this.selectionStates.clear()
|
||||||
|
this.temporarySelectionStates.forEach((value, key) => {
|
||||||
|
this.selectionStates.set(key, value)
|
||||||
|
})
|
||||||
|
this._logicalOperator = this.temporaryLogicalOperator
|
||||||
|
this._intersection = this.temporaryIntersection
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(complete: boolean = false) {
|
||||||
|
this.temporarySelectionStates.clear()
|
||||||
|
if (complete) {
|
||||||
|
this.selectionStates.clear()
|
||||||
|
} else {
|
||||||
|
this.selectionStates.forEach((value, key) => {
|
||||||
|
this.temporarySelectionStates.set(key, value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diff(): ChangedItems {
|
||||||
|
return {
|
||||||
|
itemsToAdd: this.items.filter(
|
||||||
|
(item) =>
|
||||||
|
this.temporarySelectionStates.get(item.id) ==
|
||||||
|
ToggleableItemState.Selected &&
|
||||||
|
this.selectionStates.get(item.id) != ToggleableItemState.Selected
|
||||||
|
),
|
||||||
|
itemsToRemove: this.items.filter(
|
||||||
|
(item) =>
|
||||||
|
!this.temporarySelectionStates.has(item.id) &&
|
||||||
|
this.selectionStates.has(item.id)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-filterable-dropdown',
|
||||||
|
templateUrl: './filterable-dropdown.component.html',
|
||||||
|
styleUrls: ['./filterable-dropdown.component.scss'],
|
||||||
|
})
|
||||||
|
export class FilterableDropdownComponent {
|
||||||
|
@ViewChild('listFilterTextInput') listFilterTextInput: ElementRef
|
||||||
|
@ViewChild('dropdown') dropdown: NgbDropdown
|
||||||
|
|
||||||
|
filterText: string
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set items(items: MatchingModel[]) {
|
||||||
|
if (items) {
|
||||||
|
this._selectionModel.items = Array.from(items)
|
||||||
|
this._selectionModel.items.unshift({
|
||||||
|
name: $localize`:Filter drop down element to filter for documents with no correspondent/type/tag assigned:Not assigned`,
|
||||||
|
id: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get items(): MatchingModel[] {
|
||||||
|
return this._selectionModel.items
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectionModel: FilterableDropdownSelectionModel =
|
||||||
|
new FilterableDropdownSelectionModel()
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set selectionModel(model: FilterableDropdownSelectionModel) {
|
||||||
|
if (this.selectionModel) {
|
||||||
|
this.selectionModel.changed.complete()
|
||||||
|
model.items = this.selectionModel.items
|
||||||
|
model.manyToOne = this.selectionModel.manyToOne
|
||||||
|
model.singleSelect = this.editing && !this.selectionModel.manyToOne
|
||||||
|
}
|
||||||
|
model.changed.subscribe((updatedModel) => {
|
||||||
|
this.selectionModelChange.next(updatedModel)
|
||||||
|
})
|
||||||
|
this._selectionModel = model
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectionModel(): FilterableDropdownSelectionModel {
|
||||||
|
return this._selectionModel
|
||||||
|
}
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
selectionModelChange = new EventEmitter<FilterableDropdownSelectionModel>()
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set manyToOne(manyToOne: boolean) {
|
||||||
|
this.selectionModel.manyToOne = manyToOne
|
||||||
|
}
|
||||||
|
|
||||||
|
get manyToOne() {
|
||||||
|
return this.selectionModel.manyToOne
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
title: string
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
filterPlaceholder: string = ''
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
icon: string
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
allowSelectNone: boolean = false
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
editing = false
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
applyOnClose = false
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
disabled = false
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
apply = new EventEmitter<ChangedItems>()
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
opened = new EventEmitter()
|
||||||
|
|
||||||
|
get modifierToggleEnabled(): boolean {
|
||||||
|
return this.manyToOne
|
||||||
|
? this.selectionModel.selectionSize() > 1 &&
|
||||||
|
this.selectionModel.getExcludedItems().length == 0
|
||||||
|
: !this.selectionModel.isNoneSelected()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
documentCounts: SelectionDataItem[]
|
||||||
|
|
||||||
|
get name(): string {
|
||||||
|
return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null
|
||||||
|
}
|
||||||
|
|
||||||
|
getUpdatedDocumentCount(id: number) {
|
||||||
|
if (this.documentCounts) {
|
||||||
|
return this.documentCounts.find((c) => c.id === id)?.document_count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modelIsDirty: boolean = false
|
||||||
|
|
||||||
|
constructor(private filterPipe: FilterPipe) {
|
||||||
|
this.selectionModelChange.subscribe((updatedModel) => {
|
||||||
|
this.modelIsDirty = updatedModel.isDirty()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
applyClicked() {
|
||||||
|
if (this.selectionModel.isDirty()) {
|
||||||
|
this.dropdown.close()
|
||||||
|
if (!this.applyOnClose) {
|
||||||
|
this.apply.emit(this.selectionModel.diff())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dropdownOpenChange(open: boolean): void {
|
||||||
|
if (open) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.listFilterTextInput.nativeElement.focus()
|
||||||
|
}, 0)
|
||||||
|
if (this.editing) {
|
||||||
|
this.selectionModel.reset()
|
||||||
|
this.modelIsDirty = false
|
||||||
|
}
|
||||||
|
this.opened.next(this)
|
||||||
|
} else {
|
||||||
|
this.filterText = ''
|
||||||
|
if (this.applyOnClose && this.selectionModel.isDirty()) {
|
||||||
|
this.apply.emit(this.selectionModel.diff())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listFilterEnter(): void {
|
||||||
|
let filtered = this.filterPipe.transform(this.items, this.filterText)
|
||||||
|
if (filtered.length == 1) {
|
||||||
|
this.selectionModel.toggle(filtered[0].id)
|
||||||
|
if (this.editing) {
|
||||||
|
this.applyClicked()
|
||||||
|
} else {
|
||||||
|
this.dropdown.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
excludeClicked(itemID: number) {
|
||||||
|
if (this.editing) {
|
||||||
|
this.selectionModel.toggle(itemID)
|
||||||
|
} else {
|
||||||
|
this.selectionModel.exclude(itemID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.selectionModel.reset(true)
|
||||||
|
this.selectionModelChange.emit(this.selectionModel)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="toggleItem($event)" [disabled]="disabled">
|
||||||
|
<div class="selected-icon me-1">
|
||||||
|
<ng-container *ngIf="isChecked()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16">
|
||||||
|
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
|
||||||
|
</svg>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="isPartiallyChecked()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-dash" viewBox="0 0 16 16">
|
||||||
|
<path d="M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8z"/>
|
||||||
|
</svg>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="isExcluded()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-x" viewBox="0 0 16 16">
|
||||||
|
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
||||||
|
</svg>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
<div class="me-1">
|
||||||
|
<app-tag *ngIf="isTag; else displayName" [tag]="item" [clickable]="false"></app-tag>
|
||||||
|
<ng-template #displayName><small>{{item.name}}</small></ng-template>
|
||||||
|
</div>
|
||||||
|
<div class="badge badge-light rounded-pill ms-auto me-1">{{count ?? item.document_count}}</div>
|
||||||
|
</button>
|
@ -0,0 +1,4 @@
|
|||||||
|
.selected-icon {
|
||||||
|
min-width: 1em;
|
||||||
|
min-height: 1em;
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
import { Component, EventEmitter, Input, Output, OnInit } from '@angular/core'
|
||||||
|
import { MatchingModel } from 'src/app/data/matching-model'
|
||||||
|
|
||||||
|
export enum ToggleableItemState {
|
||||||
|
NotSelected = 0,
|
||||||
|
Selected = 1,
|
||||||
|
PartiallySelected = 2,
|
||||||
|
Excluded = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-toggleable-dropdown-button',
|
||||||
|
templateUrl: './toggleable-dropdown-button.component.html',
|
||||||
|
styleUrls: ['./toggleable-dropdown-button.component.scss'],
|
||||||
|
})
|
||||||
|
export class ToggleableDropdownButtonComponent {
|
||||||
|
@Input()
|
||||||
|
item: MatchingModel
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
state: ToggleableItemState
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
count: number
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
disabled: boolean = false
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
toggle = new EventEmitter()
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
exclude = new EventEmitter()
|
||||||
|
|
||||||
|
get isTag(): boolean {
|
||||||
|
return 'is_inbox_tag' in this.item
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleItem(event: MouseEvent): void {
|
||||||
|
if (this.state == ToggleableItemState.Selected) {
|
||||||
|
this.exclude.emit()
|
||||||
|
} else {
|
||||||
|
this.toggle.emit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isChecked() {
|
||||||
|
return this.state == ToggleableItemState.Selected
|
||||||
|
}
|
||||||
|
|
||||||
|
isPartiallyChecked() {
|
||||||
|
return this.state == ToggleableItemState.PartiallySelected
|
||||||
|
}
|
||||||
|
|
||||||
|
isExcluded() {
|
||||||
|
return this.state == ToggleableItemState.Excluded
|
||||||
|
}
|
||||||
|
}
|
54
src-ui/src/app/components/common/input/abstract-input.ts
Normal file
54
src-ui/src/app/components/common/input/abstract-input.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { Directive, ElementRef, Input, OnInit, ViewChild } from '@angular/core'
|
||||||
|
import { ControlValueAccessor } from '@angular/forms'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class AbstractInputComponent<T> implements OnInit, ControlValueAccessor {
|
||||||
|
@ViewChild('inputField')
|
||||||
|
inputField: ElementRef
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
onChange = (newValue: T) => {}
|
||||||
|
|
||||||
|
onTouched = () => {}
|
||||||
|
|
||||||
|
writeValue(newValue: any): void {
|
||||||
|
this.value = newValue
|
||||||
|
}
|
||||||
|
registerOnChange(fn: any): void {
|
||||||
|
this.onChange = fn
|
||||||
|
}
|
||||||
|
registerOnTouched(fn: any): void {
|
||||||
|
this.onTouched = fn
|
||||||
|
}
|
||||||
|
setDisabledState?(isDisabled: boolean): void {
|
||||||
|
this.disabled = isDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
if (this.inputField && this.inputField.nativeElement) {
|
||||||
|
this.inputField.nativeElement.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
title: string
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
disabled = false
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
error: string
|
||||||
|
|
||||||
|
value: T
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.inputId = uuidv4()
|
||||||
|
}
|
||||||
|
|
||||||
|
inputId: string
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
hint: string
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
<div class="mb-3 form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()" [disabled]="disabled">
|
||||||
|
<label class="form-check-label" [for]="inputId">{{title}}</label>
|
||||||
|
<div *ngIf="hint" class="form-text text-muted">{{hint}}</div>
|
||||||
|
</div>
|
@ -0,0 +1,22 @@
|
|||||||
|
import { Component, forwardRef, Input, OnInit } from '@angular/core'
|
||||||
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { AbstractInputComponent } from '../abstract-input'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
useExisting: forwardRef(() => CheckComponent),
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selector: 'app-input-check',
|
||||||
|
templateUrl: './check.component.html',
|
||||||
|
styleUrls: ['./check.component.scss'],
|
||||||
|
})
|
||||||
|
export class CheckComponent extends AbstractInputComponent<boolean> {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
<div class="mb-3">
|
||||||
|
<label *ngIf="title" [for]="inputId">{{title}}</label>
|
||||||
|
|
||||||
|
<div class="input-group" [class.is-invalid]="error">
|
||||||
|
<span class="input-group-text" [style.background-color]="value"> </span>
|
||||||
|
|
||||||
|
<ng-template #popContent>
|
||||||
|
<div style="min-width: 200px;" class="pb-3">
|
||||||
|
<color-slider [color]="value" (onChangeComplete)="colorChanged($event.color.hex)"></color-slider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<input class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [autoClose]="'outside'" [ngbPopover]="popContent" placement="bottom" popoverClass="shadow">
|
||||||
|
|
||||||
|
<button class="btn btn-outline-secondary" type="button" (click)="randomize()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-dice-5" viewBox="0 0 16 16">
|
||||||
|
<path d="M13 1a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h10zM3 0a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V3a3 3 0 0 0-3-3H3z"/>
|
||||||
|
<path d="M5.5 4a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm8 0a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0 8a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm-8 0a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm4-4a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
{{error}}
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,31 @@
|
|||||||
|
import { Component, forwardRef } from '@angular/core'
|
||||||
|
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||||
|
import { randomColor } from 'src/app/utils/color'
|
||||||
|
import { AbstractInputComponent } from '../abstract-input'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
useExisting: forwardRef(() => ColorComponent),
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selector: 'app-input-color',
|
||||||
|
templateUrl: './color.component.html',
|
||||||
|
styleUrls: ['./color.component.scss'],
|
||||||
|
})
|
||||||
|
export class ColorComponent extends AbstractInputComponent<string> {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
randomize() {
|
||||||
|
this.colorChanged(randomColor())
|
||||||
|
}
|
||||||
|
|
||||||
|
colorChanged(value) {
|
||||||
|
this.value = value
|
||||||
|
this.onChange(value)
|
||||||
|
}
|
||||||
|
}
|
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