diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 8ab9e0b78..2e8861abb 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -102,3 +102,14 @@ body: attributes: label: Other description: Any other relevant details. + - type: checkboxes + id: required-checks + attributes: + label: Please confirm the following + options: + - label: I believe this issue is a bug that affects all users of Paperless-ngx, not something specific to my installation. + required: true + - label: I have already searched for relevant existing issues and discussions before opening this report. + required: true + - label: I have updated the title field above with a concise description. + required: true diff --git a/docs/administration.md b/docs/administration.md index 808d6afaf..cf8a24294 100644 --- a/docs/administration.md +++ b/docs/administration.md @@ -607,3 +607,10 @@ document_fuzzy_match [--ratio] [--processes N] | ----------- | -------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | --ratio | No | 85.0 | a number between 0 and 100, setting how similar a document must be for it to be reported. Higher numbers mean more similarity. | | --processes | No | 1/4 of system cores | Number of processes to use for matching. Setting 1 disables multiple processes | +| --delete | No | False | If provided, one document of a matched pair above the ratio will be deleted. | + +!!! warning + + If providing the `--delete` option, it is highly recommended to have a backup. + While every effort has been taken to ensure proper operation, there is always the + chance of deletion of a file you want to keep. diff --git a/docs/assets/screenshots/custom_field2.png b/docs/assets/screenshots/custom_field2.png index f7bf012dd..0bc2168a6 100644 Binary files a/docs/assets/screenshots/custom_field2.png and b/docs/assets/screenshots/custom_field2.png differ diff --git a/docs/assets/screenshots/editing.png b/docs/assets/screenshots/editing.png index e70b2891e..8026f6e05 100644 Binary files a/docs/assets/screenshots/editing.png and b/docs/assets/screenshots/editing.png differ diff --git a/docs/assets/screenshots/permissions_document.png b/docs/assets/screenshots/permissions_document.png index 9b05f104d..887cc2fc6 100644 Binary files a/docs/assets/screenshots/permissions_document.png and b/docs/assets/screenshots/permissions_document.png differ diff --git a/docs/changelog.md b/docs/changelog.md index 798387544..fc88e8110 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,38 @@ # Changelog +## paperless-ngx 2.1.2 + +### Bug Fixes + +- Fix: sort consumption templates by order by default [@shamoon](https://github.com/shamoon) ([#4956](https://github.com/paperless-ngx/paperless-ngx/pull/4956)) +- Fix: Updates gotenberg-client, including workaround for Gotenberg non-latin handling [@stumpylog](https://github.com/stumpylog) ([#4944](https://github.com/paperless-ngx/paperless-ngx/pull/4944)) +- Fix: allow text copy in pngx pdf viewer [@shamoon](https://github.com/shamoon) ([#4938](https://github.com/paperless-ngx/paperless-ngx/pull/4938)) +- Fix: Don't allow autocomplete searches to fail on schema field matches [@stumpylog](https://github.com/stumpylog) ([#4934](https://github.com/paperless-ngx/paperless-ngx/pull/4934)) +- Fix: Convert search dates to UTC in advanced search [@bogdal](https://github.com/bogdal) ([#4891](https://github.com/paperless-ngx/paperless-ngx/pull/4891)) +- Fix: Use the attachment filename so downstream template matching works [@stumpylog](https://github.com/stumpylog) ([#4931](https://github.com/paperless-ngx/paperless-ngx/pull/4931)) +- Fix: frontend handle autocomplete failure gracefully [@shamoon](https://github.com/shamoon) ([#4903](https://github.com/paperless-ngx/paperless-ngx/pull/4903)) + +### Dependencies + +- Chore(deps-dev): Bump the small-changes group with 2 updates [@dependabot](https://github.com/dependabot) ([#4942](https://github.com/paperless-ngx/paperless-ngx/pull/4942)) +- Chore(deps-dev): Bump the development group with 1 update [@dependabot](https://github.com/dependabot) ([#4939](https://github.com/paperless-ngx/paperless-ngx/pull/4939)) + +### All App Changes + +
+9 changes + +- Fix: sort consumption templates by order by default [@shamoon](https://github.com/shamoon) ([#4956](https://github.com/paperless-ngx/paperless-ngx/pull/4956)) +- Chore: reorganize api tests [@shamoon](https://github.com/shamoon) ([#4935](https://github.com/paperless-ngx/paperless-ngx/pull/4935)) +- Chore(deps-dev): Bump the small-changes group with 2 updates [@dependabot](https://github.com/dependabot) ([#4942](https://github.com/paperless-ngx/paperless-ngx/pull/4942)) +- Fix: allow text copy in pngx pdf viewer [@shamoon](https://github.com/shamoon) ([#4938](https://github.com/paperless-ngx/paperless-ngx/pull/4938)) +- Chore(deps-dev): Bump the development group with 1 update [@dependabot](https://github.com/dependabot) ([#4939](https://github.com/paperless-ngx/paperless-ngx/pull/4939)) +- Fix: Don't allow autocomplete searches to fail on schema field matches [@stumpylog](https://github.com/stumpylog) ([#4934](https://github.com/paperless-ngx/paperless-ngx/pull/4934)) +- Fix: Convert search dates to UTC in advanced search [@bogdal](https://github.com/bogdal) ([#4891](https://github.com/paperless-ngx/paperless-ngx/pull/4891)) +- Fix: Use the attachment filename so downstream template matching works [@stumpylog](https://github.com/stumpylog) ([#4931](https://github.com/paperless-ngx/paperless-ngx/pull/4931)) +- Fix: frontend handle autocomplete failure gracefully [@shamoon](https://github.com/shamoon) ([#4903](https://github.com/paperless-ngx/paperless-ngx/pull/4903)) +
+ ## paperless-ngx 2.1.1 ### Bug Fixes diff --git a/docs/configuration.md b/docs/configuration.md index 87d992443..b2e479f98 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -733,7 +733,7 @@ they use underscores instead of dashes. Paperless has been tested to work with the OCR options provided above. There are many options that are incompatible with each other, so specifying invalid options may prevent paperless from consuming - any documents. + any documents. Use with caution! Specify arguments as a JSON dictionary. Keep note of lower case booleans and double quoted parameter names and strings. Examples: diff --git a/src-ui/e2e/document-detail/document-detail.spec.ts b/src-ui/e2e/document-detail/document-detail.spec.ts index e40b88ccc..0cd45a058 100644 --- a/src-ui/e2e/document-detail/document-detail.spec.ts +++ b/src-ui/e2e/document-detail/document-detail.spec.ts @@ -12,13 +12,9 @@ test('should activate / deactivate save button when changes are saved', async ({ await expect(page.getByTitle('Storage path', { exact: true })).toHaveText( /\w+/ ) - await expect( - page.getByRole('button', { name: 'Save', exact: true }) - ).toBeDisabled() + await expect(page.getByRole('button', { name: 'Save' }).nth(1)).toBeDisabled() await page.getByTitle('Storage path').getByTitle('Clear all').click() - await expect( - page.getByRole('button', { name: 'Save', exact: true }) - ).toBeEnabled() + await expect(page.getByRole('button', { name: 'Save' }).nth(1)).toBeEnabled() }) test('should warn on unsaved changes', async ({ page }) => { @@ -27,16 +23,12 @@ test('should warn on unsaved changes', async ({ page }) => { await expect(page.getByTitle('Correspondent', { exact: true })).toHaveText( /\w+/ ) - await expect( - page.getByRole('button', { name: 'Save', exact: true }) - ).toBeDisabled() + await expect(page.getByRole('button', { name: 'Save' }).nth(1)).toBeDisabled() await page .getByTitle('Storage path', { exact: true }) .getByTitle('Clear all') .click() - await expect( - page.getByRole('button', { name: 'Save', exact: true }) - ).toBeEnabled() + await expect(page.getByRole('button', { name: 'Save' }).nth(1)).toBeEnabled() await page.getByRole('button', { name: 'Close', exact: true }).click() await expect(page.getByRole('dialog')).toHaveText(/unsaved changes/) await page.getByRole('button', { name: 'Cancel' }).click() diff --git a/src-ui/src/app/components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component.html b/src-ui/src/app/components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component.html index 920026448..f537527eb 100644 --- a/src-ui/src/app/components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component.html +++ b/src-ui/src/app/components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component.html @@ -17,7 +17,7 @@
Filters

Process documents that match all filters specified below.

- + diff --git a/src-ui/src/app/components/common/input/date/date.component.spec.ts b/src-ui/src/app/components/common/input/date/date.component.spec.ts index 2b5467412..766d7fa02 100644 --- a/src-ui/src/app/components/common/input/date/date.component.spec.ts +++ b/src-ui/src/app/components/common/input/date/date.component.spec.ts @@ -81,6 +81,16 @@ describe('DateComponent', () => { expect(eventSpy).toHaveBeenCalled() }) + it('should show allow system keyboard events', () => { + let event: KeyboardEvent = new KeyboardEvent('keypress', { + key: '9', + altKey: true, + }) + let preventDefaultSpy = jest.spyOn(event, 'preventDefault') + input.dispatchEvent(event) + expect(preventDefaultSpy).not.toHaveBeenCalled() + }) + it('should support paste', () => { expect(component.value).toBeUndefined() const date = '5/4/20' @@ -99,5 +109,25 @@ describe('DateComponent', () => { event['clipboardData'] = clipboardData input.dispatchEvent(event) expect(component.value).toEqual({ day: 4, month: 5, year: 2020 }) + // coverage + window['clipboardData'] = { + getData: (type) => '', + } + component.onPaste(new Event('foo') as any) + }) + + it('should set filter button title', () => { + component.title = 'foo' + expect(component.filterButtonTitle).toEqual( + 'Filter documents with this foo' + ) + }) + + it('should emit date on filter', () => { + let dateReceived + component.value = '12/16/2023' + component.filterDocuments.subscribe((date) => (dateReceived = date)) + component.onFilterDocuments() + expect(dateReceived).toEqual([{ day: 16, month: 12, year: 2023 }]) }) }) diff --git a/src-ui/src/app/components/common/input/date/date.component.ts b/src-ui/src/app/components/common/input/date/date.component.ts index 36bbea57c..9d96379e3 100644 --- a/src-ui/src/app/components/common/input/date/date.component.ts +++ b/src-ui/src/app/components/common/input/date/date.component.ts @@ -90,7 +90,11 @@ export class DateComponent } onKeyPress(event: KeyboardEvent) { - if ('Enter' !== event.key && !/[0-9,\.\/-]+/.test(event.key)) { + if ( + 'Enter' !== event.key && + !(event.altKey || event.metaKey || event.ctrlKey) && + !/[0-9,\.\/-]+/.test(event.key) + ) { event.preventDefault() } } diff --git a/src-ui/src/app/components/common/input/document-link/document-link.component.scss b/src-ui/src/app/components/common/input/document-link/document-link.component.scss index bcaa4e849..51f6fa055 100644 --- a/src-ui/src/app/components/common/input/document-link/document-link.component.scss +++ b/src-ui/src/app/components/common/input/document-link/document-link.component.scss @@ -1,6 +1,10 @@ -::ng-deep .ng-select-container .ng-value-container .ng-value { - background-color: transparent !important; - border-color: transparent; +::ng-deep .ng-select-container .ng-value-container { + overflow: hidden; + + .ng-value { + background-color: transparent !important; + border-color: transparent; + } } .sidebaricon { @@ -9,6 +13,4 @@ .badge { font-size: .75rem; - // --bs-primary: var(--pngx-bg-alt); - // color: var(--pngx-primary-text-contrast); } diff --git a/src-ui/src/app/components/common/input/document-link/document-link.component.spec.ts b/src-ui/src/app/components/common/input/document-link/document-link.component.spec.ts index d1af7ab2f..e00460ec5 100644 --- a/src-ui/src/app/components/common/input/document-link/document-link.component.spec.ts +++ b/src-ui/src/app/components/common/input/document-link/document-link.component.spec.ts @@ -20,6 +20,10 @@ const documents = [ id: 12, title: 'Document 12 bar', }, + { + id: 16, + title: 'Document 16 bar', + }, { id: 23, title: 'Document 23 bar', @@ -48,10 +52,15 @@ describe('DocumentLinkComponent', () => { fixture.detectChanges() }) - it('should retrieve selected documents from APIs', () => { - const getSpy = jest.spyOn(documentService, 'getCachedMany') + it('should retrieve selected documents from API', () => { + const getSpy = jest.spyOn(documentService, 'getFew') getSpy.mockImplementation((ids) => { - return of(documents.filter((d) => ids.includes(d.id))) + const docs = documents.filter((d) => ids.includes(d.id)) + return of({ + count: docs.length, + all: docs.map((d) => d.id), + results: docs, + }) }) component.writeValue([1]) expect(getSpy).toHaveBeenCalled() @@ -85,12 +94,18 @@ describe('DocumentLinkComponent', () => { }) it('should load values correctly', () => { - jest.spyOn(documentService, 'getCachedMany').mockImplementation((ids) => { - return of(documents.filter((d) => ids.includes(d.id))) + const getSpy = jest.spyOn(documentService, 'getFew') + getSpy.mockImplementation((ids) => { + const docs = documents.filter((d) => ids.includes(d.id)) + return of({ + count: docs.length, + all: docs.map((d) => d.id), + results: docs, + }) }) component.writeValue([12, 23]) expect(component.value).toEqual([12, 23]) - expect(component.selectedDocuments).toEqual([documents[1], documents[2]]) + expect(component.selectedDocuments).toEqual([documents[1], documents[3]]) component.writeValue(null) expect(component.value).toEqual([]) expect(component.selectedDocuments).toEqual([]) @@ -100,9 +115,14 @@ describe('DocumentLinkComponent', () => { }) it('should support unselect', () => { - const getSpy = jest.spyOn(documentService, 'getCachedMany') + const getSpy = jest.spyOn(documentService, 'getFew') getSpy.mockImplementation((ids) => { - return of(documents.filter((d) => ids.includes(d.id))) + const docs = documents.filter((d) => ids.includes(d.id)) + return of({ + count: docs.length, + all: docs.map((d) => d.id), + results: docs, + }) }) component.writeValue([12, 23]) component.unselect({ id: 23 }) @@ -115,4 +135,26 @@ describe('DocumentLinkComponent', () => { expect(component.compareDocuments(documents[0], { id: 2 })).toBeFalsy() expect(component.trackByFn(documents[1])).toEqual(12) }) + + it('should not include the current document or already selected documents in results', () => { + let foundDocs + component.foundDocuments$.subscribe((found) => (foundDocs = found)) + component.parentDocumentID = 23 + component.selectedDocuments = [documents[2]] + const listSpy = jest.spyOn(documentService, 'listFiltered') + listSpy.mockImplementation( + (page, pageSize, sortField, sortReverse, filterRules, extraParams) => { + const docs = documents.filter((d) => + d.title.includes(filterRules[0].value) + ) + return of({ + count: docs.length, + results: docs, + all: docs.map((d) => d.id), + }) + } + ) + component.documentsInput$.next('bar') + expect(foundDocs).toEqual([documents[1]]) + }) }) diff --git a/src-ui/src/app/components/common/input/document-link/document-link.component.ts b/src-ui/src/app/components/common/input/document-link/document-link.component.ts index dd7118074..77a0fb99a 100644 --- a/src-ui/src/app/components/common/input/document-link/document-link.component.ts +++ b/src-ui/src/app/components/common/input/document-link/document-link.component.ts @@ -43,6 +43,9 @@ export class DocumentLinkComponent @Input() notFoundText: string = $localize`No documents found` + @Input() + parentDocumentID: number + constructor(private documentsService: DocumentService) { super() } @@ -58,11 +61,11 @@ export class DocumentLinkComponent } else { this.loading = true this.documentsService - .getCachedMany(documentIDs) + .getFew(documentIDs, { fields: 'id,title' }) .pipe(takeUntil(this.unsubscribeNotifier)) - .subscribe((documents) => { + .subscribe((documentResults) => { this.loading = false - this.selectedDocuments = documents + this.selectedDocuments = documentResults.results super.writeValue(documentIDs) }) } @@ -86,7 +89,13 @@ export class DocumentLinkComponent { truncate_content: true } ) .pipe( - map((results) => results.results), + map((results) => + results.results.filter( + (d) => + d.id !== this.parentDocumentID && + !this.selectedDocuments.find((sd) => sd.id === d.id) + ) + ), catchError(() => of([])), // empty on error tap(() => (this.loading = false)) ) diff --git a/src-ui/src/app/components/common/input/select/select.component.html b/src-ui/src/app/components/common/input/select/select.component.html index 194c40904..844ea1932 100644 --- a/src-ui/src/app/components/common/input/select/select.component.html +++ b/src-ui/src/app/components/common/input/select/select.component.html @@ -9,7 +9,7 @@
-
+
+
+ {{error}} +
{{hint}} Suggestions:  diff --git a/src-ui/src/app/components/common/input/select/select.component.scss b/src-ui/src/app/components/common/input/select/select.component.scss index 2ed6995c8..7cfe14fdc 100644 --- a/src-ui/src/app/components/common/input/select/select.component.scss +++ b/src-ui/src/app/components/common/input/select/select.component.scss @@ -17,3 +17,12 @@ font-style: italic; opacity: .75; } + +::ng-deep .is-invalid ng-select .ng-select-container input { + // replicate bootstrap + padding-right: calc(1.5em + 0.75rem) !important; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") !important; + background-repeat: no-repeat !important; + background-position: right calc(0.375em + 0.1875rem) center !important; + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem) !important; +} diff --git a/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.spec.ts b/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.spec.ts index 0b300bd74..7dcda57a7 100644 --- a/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.spec.ts +++ b/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.spec.ts @@ -11,7 +11,6 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { NgbAccordionModule, NgbActiveModal, - NgbModal, NgbModalModule, } from '@ng-bootstrap/ng-bootstrap' import { HttpClientModule } from '@angular/common/http' diff --git a/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts b/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts index 19391ce28..d89d49829 100644 --- a/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts +++ b/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts @@ -130,7 +130,8 @@ export class ProfileEditDialogComponent implements OnInit, OnDestroy { } save(): void { - const passwordChanged = this.currentPassword !== this.newPassword + const passwordChanged = + this.newPassword && this.currentPassword !== this.newPassword const profile = Object.assign({}, this.form.value) this.networkActive = true this.profileService diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html index b3ea4cc43..3211e60ed 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.html +++ b/src-ui/src/app/components/document-detail/document-detail.component.html @@ -96,14 +96,7 @@
-
- - - - - - -
+