Fix line endings

This commit is contained in:
Martin Tan 2023-05-15 04:50:04 +08:00
parent 565cbb1e31
commit f18ea6cf34
343 changed files with 0 additions and 216000 deletions

View File

@ -1,16 +0,0 @@
# 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

View File

@ -1,51 +0,0 @@
{
"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
View File

@ -1,51 +0,0 @@
# 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/**/*

View File

@ -1,27 +0,0 @@
# 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).

View File

@ -1,206 +0,0 @@
{
"$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
}
}
}

View File

@ -1,14 +0,0 @@
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',
},
})

View File

@ -1,68 +0,0 @@
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')
})
})

View File

@ -1,114 +0,0 @@
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')
})
})

View File

@ -1,196 +0,0 @@
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')
})
})

View File

@ -1,341 +0,0 @@
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')
})
})

View File

@ -1,25 +0,0 @@
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')
})
})

View File

@ -1,182 +0,0 @@
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)
})
})

View File

@ -1,93 +0,0 @@
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')
})
})
})

View File

@ -1,257 +0,0 @@
{
"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"
}
]
}

View File

@ -1,25 +0,0 @@
{
"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
}
]
}

View File

@ -1 +0,0 @@
{"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"}]}

View File

@ -1,26 +0,0 @@
[
{
"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
}
]

View File

@ -1 +0,0 @@
{"correspondents":[],"tags":[3],"document_types":[1]}

View File

@ -1,144 +0,0 @@
{
"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": []
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

View File

@ -1,293 +0,0 @@
{
"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": []
}

View File

@ -1,119 +0,0 @@
{
"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"
]
}
]
}

View File

@ -1,27 +0,0 @@
{
"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"
}
]
}

View File

@ -1,31 +0,0 @@
{
"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
}
]
}

View File

@ -1 +0,0 @@
{"version":"v1.7.1","update_available":false,"feature_is_set":true}

View File

@ -1,44 +0,0 @@
{
"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"
}
]
}
]
}

View File

@ -1,17 +0,0 @@
{
"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
}
]
}

View File

@ -1,103 +0,0 @@
{
"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
}
]
}

View File

@ -1,142 +0,0 @@
[
{
"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
}
]

View File

@ -1,163 +0,0 @@
{
"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"
]
}

View File

@ -1,88 +0,0 @@
{
"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"
]
}

View File

@ -1,459 +0,0 @@
{
"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"
]
}
]
}

View File

@ -1,3 +0,0 @@
// 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) => {}

View File

@ -1,43 +0,0 @@
// ***********************************************
// 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) => { ... })

View File

@ -1,55 +0,0 @@
// 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',
})
})

View File

@ -1,8 +0,0 @@
{
"extends": "../tsconfig.json",
"include": ["**/*.ts"],
"compilerOptions": {
"sourceMap": false,
"types": ["cypress"]
}
}

View File

@ -1,36 +0,0 @@
// @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,
},
})
)
},
}

View File

@ -1,25 +0,0 @@
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)
)
})
})

View File

@ -1,13 +0,0 @@
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>
}
}

View File

@ -1,14 +0,0 @@
/* 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"
]
}
}

View File

@ -1,8 +0,0 @@
module.exports = {
moduleNameMapper: {
'@core/(.*)': '<rootDir>/src/app/core/$1',
},
preset: 'jest-preset-angular',
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
testPathIgnorePatterns: ['/node_modules/', '/cypress/'],
}

File diff suppressed because it is too large Load Diff

18796
src-ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,68 +0,0 @@
{
"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"
}
}

View File

@ -1,31 +0,0 @@
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()

View File

@ -1,197 +0,0 @@
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 {}

View File

@ -1,38 +0,0 @@
<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>

View File

@ -1,283 +0,0 @@
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)
}
}

View File

@ -1,255 +0,0 @@
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 {}

View File

@ -1,257 +0,0 @@
<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>&nbsp;<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>&nbsp;<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>&nbsp;{{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>&nbsp;{{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>&nbsp;<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>&nbsp;<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>&nbsp;<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>&nbsp;<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>&nbsp;<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>&nbsp;<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>&nbsp;<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>&nbsp;<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>&nbsp;<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>&nbsp;<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>

View File

@ -1,311 +0,0 @@
@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%;
}
}

View File

@ -1,231 +0,0 @@
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()
}
}
}

View File

@ -1,9 +0,0 @@
<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>

View File

@ -1,28 +0,0 @@
.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);
}
}

View File

@ -1,33 +0,0 @@
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()
}
}

View File

@ -1,22 +0,0 @@
<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>

View File

@ -1,88 +0,0 @@
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()
}
}

View File

@ -1,64 +0,0 @@
<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>

View File

@ -1,12 +0,0 @@
.date-dropdown {
min-width: 250px;
.btn-link {
line-height: 1;
}
}
.selected-icon {
min-width: 1em;
min-height: 1em;
}

View File

@ -1,158 +0,0 @@
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()
}
}
}

View File

@ -1,23 +0,0 @@
<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>

View File

@ -1,41 +0,0 @@
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),
})
}
}

View File

@ -1,25 +0,0 @@
<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>

View File

@ -1,41 +0,0 @@
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),
})
}
}

View File

@ -1,143 +0,0 @@
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()
}
}

View File

@ -1,19 +0,0 @@
<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>

View File

@ -1,37 +0,0 @@
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),
})
}
}

View File

@ -1,37 +0,0 @@
<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>

View File

@ -1,4 +0,0 @@
::ng-deep .alert-dismissible .btn-close {
padding-top: 0.75rem !important;
padding-bottom: 0.75rem !important;
}

View File

@ -1,91 +0,0 @@
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`
}
}

View File

@ -1,42 +0,0 @@
<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>

View File

@ -1,205 +0,0 @@
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
}
}

View File

@ -1,24 +0,0 @@
<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>

View File

@ -1,52 +0,0 @@
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),
})
}
}

View File

@ -1,26 +0,0 @@
<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>

View File

@ -1,44 +0,0 @@
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),
})
}
}

View File

@ -1,38 +0,0 @@
<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>

View File

@ -1,87 +0,0 @@
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()
}
}

View File

@ -1,50 +0,0 @@
<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">&nbsp;{{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>

View File

@ -1,42 +0,0 @@
.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;
}

View File

@ -1,484 +0,0 @@
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)
}
}

View File

@ -1,24 +0,0 @@
<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>

View File

@ -1,4 +0,0 @@
.selected-icon {
min-width: 1em;
min-height: 1em;
}

View File

@ -1,58 +0,0 @@
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
}
}

View File

@ -1,54 +0,0 @@
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
}

View File

@ -1,5 +0,0 @@
<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>

View File

@ -1,22 +0,0 @@
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()
}
}

View File

@ -1,28 +0,0 @@
<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">&nbsp;&nbsp;&nbsp;</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>

View File

@ -1,31 +0,0 @@
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